diff --git a/CHANGELOG.md b/CHANGELOG.md index 84e4d94..5530304 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project are documented here. The format follows ## Unreleased +### Fixed + +- `probbit monitor` opens a strand with checkpoint lines when its persona has an `expectation` block: it replays such a + strand from the header (`--once`, `--follow`, `--serve`, and each strand of `--dir` by its own persona) instead of refusing + it at the last checkpoint, which does not carry the expectation's memory. Other personas still start at the last + checkpoint; `live verify` is unchanged. + +### Documentation + - Documentation, from a dress rehearsal of the published v0.13.0 artifacts (the release page's Mac app zip opened as a download, the README and REPRODUCE.md commands with the released binary on Linux and macOS, `probbit-stance` 0.2.0 from npm): `docs/monitor-app.md` says to remove the quarantine mark before the first open and how to recover after a refused diff --git a/docs/persona.md b/docs/persona.md index 2d8e2f6..a49bfd4 100644 --- a/docs/persona.md +++ b/docs/persona.md @@ -1573,7 +1573,11 @@ check that inference. Hashes alone do not authenticate a writer or detect a whol anchor. Historical measurement before this hardening, on an Apple M4: a 10,000-event strand of a drives persona (4 goals, a floor, learning; 3,126,444 bytes, 10 checkpoint lines) opens in `monitor --once` from its last checkpoint in 0.005 s (load 3.3), where a full `verify` takes 192 s. A replay from a checkpoint costs what the events after it cost: 500 events past the last one took 26 s to draw -(load 6-13, about 50 ms an event for this persona), so K bounds the wait; a smaller K costs a few KB per checkpoint line. A strand without checkpoint or control lines (shorter than K events, or `--checkpoint-every 0`) is the +(load 6-13, about 50 ms an event for this persona), so K bounds the wait; a smaller K costs a few KB per checkpoint line. A persona with an `expectation` block (RFC 0009) +is the exception: its expectation memory is a fold of every event line, and no checkpoint line carries it, so `monitor` +(`--once`, `--follow`, `--serve`, each strand of `--dir` by its own persona) replays such a strand from its header, every +line, and `verify --from-checkpoint` still refuses it. The cost is a full replay when the strand is opened: on a 16-vCPU Linux VM, a 5,000-event life of the tutor with the block (10 checkpoint lines) opens in `monitor --once` in 7.4 to 7.8 s, about 650 events a second, where the same life without the block opens from its last checkpoint in under 0.05 s; after +that, appended lines replay one by one from the state in memory, as for any strand. A strand without checkpoint or control lines (shorter than K events, or `--checkpoint-every 0`) is the same format as in 0.8.0. New strand headers record the current engine version, so their chain hashes differ; existing 0.8.0 strands remain replayable. Binaries before 0.8.0 do not read checkpoint or control lines. diff --git a/probbit-cli/src/monitor.rs b/probbit-cli/src/monitor.rs index b0ac76d..614dfe2 100644 --- a/probbit-cli/src/monitor.rs +++ b/probbit-cli/src/monitor.rs @@ -127,6 +127,13 @@ impl Watch { let rep = Replay::open(head)?; let meta = Meta::of(&rep); let n = meta.moods.len(); Ok(Watch { rep, meta, last: None, spark: vec![VecDeque::new(); n], bad: None }) } + /// Where the replay of `lines` (the header first) starts: the last checkpoint line (index), or None for the header. RFC 0009: + /// a persona with an expectation block folds every event line into its memory, which no checkpoint carries, so its strand + /// replays from the header (every line, slower; `open_at` refuses that shortcut); any other persona starts at the last one + fn start(lines: &[&str]) -> Option { + let k = crate::live::last_checkpoint(lines, 0).filter(|k| *k >= 2)?; + (!Replay::open(lines[0]).is_ok_and(|r| r.live.p.expect.is_some())).then_some(k) + } /// The replay from a checkpoint line (`live::Replay::open_at`; `before` = the event line before it): the lines before it are /// not replayed, and the board starts at the checkpoint's event (its stance, the inputs `before` logs, its learned deltas) fn open_at(head: &str, before: &str, cp: &str, at: usize) -> Result { @@ -644,10 +651,11 @@ impl Followed { fn poll(&mut self, eng: Engine, each: &mut dyn FnMut(&Watch)) -> Option { match self.tail.poll() { Got::Lines(ls) => { - // the first read of a strand with checkpoint lines starts at the last one: its event is drawn at once + // the first read of a strand with checkpoint lines starts at the last one (`Watch::start`): its event is drawn at + // once; after that, appended lines replay from the state in memory let mut skip = 0; if self.w.is_none() && self.bad_head.is_none() { let refs: Vec<&str> = ls.iter().map(String::as_str).collect(); - if let Some(k) = crate::live::last_checkpoint(&refs, 0).filter(|k| *k >= 2) { + if let Some(k) = Watch::start(&refs) { if let Ok(x) = Watch::open_at(refs[0], refs[k - 1], refs[k], k + 1) { each(&x); self.w = Some(x); skip = k + 1; } } } for l in ls.into_iter().skip(skip) { if let Some(x) = &mut self.w { let n = x.rep.live.n; x.feed(&l, eng); if x.rep.live.n != n && x.bad.is_none() { each(x); } continue; } if self.bad_head.is_none() { match Watch::open(&l) { Ok(x) => self.w = Some(x), Err(e) => self.bad_head = Some(e) } } } @@ -746,8 +754,9 @@ pub fn cmd(args: &[String]) { if head.is_empty() { crate::fail(&format!("monitor: {path} is empty: not a strand")) } if !strand(head) { crate::fail(&format!("monitor: {path} is not a probbit strand (format 1)")) } let note = (!rest.is_empty() && !lines.is_empty()).then(|| format!("line {} is incomplete (no newline at its end): not replayed", lines.len() + 1)); - // a strand with checkpoint lines replays from the last one (the board starts at its event and draws the latest one) - let start = crate::live::last_checkpoint(&lines, 0); + // a strand with checkpoint lines replays from the last one (the board starts at its event and draws the latest one), unless + // its persona has an expectation block: then from the header (`Watch::start`) + let start = Watch::start(&lines); let opened = match start { Some(k) => Watch::open_at(head, lines[k - 1], lines[k], k + 1), None => Watch::open(head) }; let (w, bad_head) = match opened { Ok(mut w) => { for l in lines.iter().skip(start.map_or(1, |k| k + 1)) { w.feed(l, eng); } (Some(w), None) } Err(e) => (None, Some(e)) }; let fr = frame(w.as_ref(), bad_head.as_ref(), &path, note.as_deref(), &s); @@ -1158,6 +1167,38 @@ mod tests { let _ = std::fs::remove_file(&f); } + /// RFC 0009: a persona with an expectation block replays a strand with checkpoint lines from its header (the checkpoint does + /// not carry the expectation's memory): `--once` and the first poll of `--follow` / `--serve` draw the latest event with no + /// checkpoint start, and lines appended after it replay from the state in memory, one by one + #[test] + fn an_expectation_persona_replays_from_the_header() { + let (p, doc) = persona::load(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/monitor/tutor-expect.yaml")).unwrap(); + assert!(p.expect.is_some(), "the fixture has an expectation block"); + let (mut lv, header) = Live::start(p.clone(), &doc, persona::init(&p, Some(2), true, &run), Clock::Fixed, "probbit test"); + let mut text = format!("{header}\n"); + let add = |lv: &mut Live, text: &mut String, t: u32| { let ev = json::parse(&format!(r#"{{"praise":{},"elapsed_hours":1}}"#, t % 3 == 0)).unwrap(); + let (s, line) = lv.event(&ev, &run).unwrap(); *text += &line; text.push('\n'); if lv.n % 5 == 0 { *text += &lv.checkpoint(&s); text.push('\n'); } }; + for t in 0..17u32 { add(&mut lv, &mut text, t); } + let lines = live::lines(&text); let k = live::last_checkpoint(&lines, 0).unwrap(); + assert_eq!((lines.iter().filter(|l| l.starts_with(r#"{"checkpoint":"#)).count(), Watch::start(&lines)), (3, None), "3 checkpoints, no checkpoint start"); + assert!(Replay::open_at(lines[0], lines[k - 1], lines[k], k + 1).is_err_and(|e| e.1.contains("expectation block")), "the shortcut itself still refuses it"); + let f = std::env::temp_dir().join(format!("probbit-monitor-expect-{}.strand", std::process::id())); std::fs::write(&f, &text).unwrap(); + let mut fl = Followed::new(f.to_str().unwrap()); let mut seen = vec![]; + assert_eq!(fl.poll(&run, &mut |w: &Watch| seen.push(w.rep.live.n)), Some(false)); + let w = fl.w.as_ref().unwrap(); assert!(w.bad.is_none(), "{:?}", w.bad); + assert_eq!((w.rep.from, w.rep.live.n, seen.first().copied(), seen.len()), (None, 17, Some(1), 17), "every event replayed from the header"); + let mut full = Watch::open(lines[0]).unwrap(); for l in &lines[1..] { full.feed(l, &run); } + assert_eq!(persona::canon(&w.last.as_ref().unwrap().doc), persona::canon(&full.last.as_ref().unwrap().doc)); + assert_eq!(frame_json(fl.w.as_ref(), None, None).get("from_checkpoint"), Some(&Json::Null)); + // appended: only the new lines replay, on the state in memory + for t in 17..21u32 { add(&mut lv, &mut text, t); } std::fs::write(&f, &text).unwrap(); + seen.clear(); assert_eq!(fl.poll(&run, &mut |w: &Watch| seen.push(w.rep.live.n)), Some(false)); + assert_eq!(seen, vec![18, 19, 20, 21]); assert!(fl.w.as_ref().unwrap().bad.is_none()); + assert_eq!(fl.w.as_ref().unwrap().rep.summary(), live::verify(&text, &run).unwrap(), "ends where verify does"); + let _ = std::fs::remove_file(&f); + // the tutor without the block takes the shortcut: `following_starts_at_the_last_checkpoint` + } + /// Watch a strand line by line: every document's sha256 is its line's `stance` digest, and the replay ends where `verify` does fn replays_to_its_digests(text: &str, name: &str) -> Watch { let lines = live::lines(text); diff --git a/probbit-cli/tests/fixtures/monitor/tutor-expect.events.jsonl b/probbit-cli/tests/fixtures/monitor/tutor-expect.events.jsonl new file mode 100644 index 0000000..5d77426 --- /dev/null +++ b/probbit-cli/tests/fixtures/monitor/tutor-expect.events.jsonl @@ -0,0 +1,22 @@ +{"praise":true,"elapsed_hours":0.5} +{"loss":true,"elapsed_hours":1} +{"elapsed_hours":2} +{"error":true,"elapsed_hours":13} +{"praise":true,"elapsed_hours":1} +{"loss":true,"elapsed_hours":0.5} +{"elapsed_hours":1} +{"error":true,"elapsed_hours":2} +{"praise":true,"elapsed_hours":13} +{"loss":true,"elapsed_hours":1} +{"elapsed_hours":0.5} +{"error":true,"elapsed_hours":1} +{"praise":true,"elapsed_hours":2} +{"loss":true,"elapsed_hours":13} +{"elapsed_hours":1} +{"error":true,"elapsed_hours":0.5} +{"praise":true,"elapsed_hours":1} +{"loss":true,"elapsed_hours":2} +{"elapsed_hours":13} +{"error":true,"elapsed_hours":1} +{"praise":true,"elapsed_hours":0.5} +{"loss":true,"elapsed_hours":1} diff --git a/probbit-cli/tests/fixtures/monitor/tutor-expect.strand b/probbit-cli/tests/fixtures/monitor/tutor-expect.strand new file mode 100644 index 0000000..57fd98a --- /dev/null +++ b/probbit-cli/tests/fixtures/monitor/tutor-expect.strand @@ -0,0 +1,27 @@ +{"probbit_strand":1,"engine":"probbit 0.13.0","persona":{"name":"Pip","version":"1.1.0","digest":"sha256:58ed82d46afa6efb027d462f488a6c5ebebefe32ddc678940a64846bfe21575c"},"seed":2,"state":{"digest":"sha256:eef51fdf809d2a63d5f9f2fe90bca59c5f319881cdebe0ce6fb35de60b7aaf92","genes":{"react":{"praise":0.7851},"shift":{"arousal":0.214,"caution":-0.3903,"curiosity":0.3383,"directness":0.0956,"emoji":0.959,"formality":-0.3644,"humour":-2.1939,"initiative":0.1127,"valence":-0.0338,"verbosity":-0.2809,"warmth":0.268}},"history":{"confused":[],"error":[]},"mood":{"arousal":[0,0,0],"valence":[0,0,0]},"persona":{"digest":"sha256:58ed82d46afa6efb027d462f488a6c5ebebefe32ddc678940a64846bfe21575c","name":"Pip","version":"1.1.0"},"prev":{"agenda":["encourage","explain","example","check"],"arousal":"steady","caution":"measured","curiosity":"exploring","directness":"gentle","emoji":"sparse","formality":"casual","humour":"light","initiative":"suggest","valence":"even","verbosity":"full","warmth":"warm"},"probbit_persona_state":1,"rest":{"arousal":"steady","caution":"measured","curiosity":"exploring","directness":"balanced","emoji":"rich","formality":"casual","humour":"light","initiative":"suggest","valence":"up","verbosity":"full","warmth":"warm"},"seed":2,"turn":0},"document":{"probbit_persona":1,"identity":{"name":"Pip","version":"1.1.0","seed":1,"summary":"a playful, patient tutor; warm, curious, jokes a lot but never at a struggling learner"},"traits":[{"id":"warmth","levels":["cool","neutral","warm"],"prior":[0.05,0.3,0.65],"spread":0.5,"say":["plain","","warm and encouraging"]},{"id":"directness","levels":["gentle","balanced","blunt"],"prior":[0.45,0.45,0.1],"spread":0.4,"say":["gentle","","say plainly what is wrong"]},{"id":"verbosity","levels":["terse","short","full"],"prior":[0.1,0.4,0.5],"spread":0.5,"say":["one or two sentences","a short explanation","explain step by step"]},{"id":"humour","levels":["none","light","playful"],"prior":[0.15,0.45,0.4],"spread":0.6,"say":["no jokes","a light joke is welcome","playful"]},{"id":"formality","levels":["casual","neutral","formal"],"prior":[0.6,0.35,0.05],"spread":0.3,"say":["casual","","formal"]},{"id":"initiative","levels":["follow","suggest","lead"],"prior":[0.2,0.5,0.3],"spread":0.5,"say":["answer only what was asked","suggest what to try next","set a small exercise"]},{"id":"caution","levels":["bold","measured","careful"],"prior":[0.25,0.55,0.2],"spread":0.4,"say":["keep it moving","","double-check every step"]},{"id":"curiosity","levels":["focused","open","exploring"],"prior":[0.15,0.4,0.45],"spread":0.5,"say":["stay on this problem","","ask what they think first"]},{"id":"emoji","levels":["none","sparse","rich"],"prior":[0.2,0.5,0.3],"spread":0.5,"say":["no emoji","one emoji at most","emoji welcome"]}],"moods":[{"id":"valence","levels":["down","even","up"],"prior":[0.1,0.5,0.4],"spread":0.4,"inertia":0.5,"half_life_hours":12},{"id":"arousal","levels":["calm","steady","keyed"],"prior":[0.3,0.4,0.3],"spread":0.3,"inertia":0.4}],"couplings":[{"vars":["valence","humour"],"align":1},{"vars":["valence","emoji"],"align":0.6},{"vars":["arousal","verbosity"],"align":-0.4},{"vars":["warmth","formality"],"align":-0.6},{"vars":["humour","formality"],"align":-0.6},{"vars":["curiosity","verbosity"],"align":0.3},{"vars":["humour","emoji"],"align":0.4}],"inputs":[{"id":"sentiment","kind":"level","levels":["negative","neutral","positive"],"default":"neutral","say":{"negative":"learner upset","positive":"learner upbeat"},"effects":{"negative":{"valence":-1,"warmth":0.6,"humour":-1,"directness":-0.6},"positive":{"valence":0.8,"humour":0.5,"emoji":0.3}}},{"id":"confused","kind":"flag","say":"learner confused","effects":{"verbosity":0.9,"humour":-0.6,"warmth":0.4,"curiosity":0.6,"initiative":0.3,"directness":-0.3}},{"id":"error","kind":"flag","say":"my last answer was wrong","effects":{"caution":1.2,"verbosity":-0.3,"valence":-0.4,"humour":-0.4}},{"id":"loss","kind":"flag","say":"learner reports a failure","effects":{"valence":-1.6,"warmth":1,"humour":-2,"emoji":-0.8}},{"id":"praise","kind":"flag","say":"praise","reactivity_spread":0.4,"effects":{"valence":1.2,"humour":0.5,"emoji":0.5}},{"id":"criticism","kind":"flag","say":"criticism","effects":{"valence":-0.6,"caution":0.6,"humour":-0.4}},{"id":"stakes","kind":"number","say":"stakes","effects":{"caution":1.4,"humour":-0.8,"emoji":-0.6}},{"id":"time_pressure","kind":"number","say":"time pressure","effects":{"verbosity":-1.8,"curiosity":-1.2,"humour":-0.4,"arousal":0.6}},{"id":"claim_done","kind":"flag","say":"about to say it is solved","effects":{"caution":0.5,"step.check":0.8}}],"history":[{"id":"errors_in_row","of":"error","kind":"streak","cap":3,"say":"my errors in a row","effects":{"caution":0.5,"humour":-0.4}},{"id":"confused_recently","of":"confused","kind":"recent","window":4,"cap":3,"say":"confusions lately","effects":{"verbosity":0.3,"warmth":0.2}}],"habits":[{"id":"no_jokes_on_loss","when":{"loss":true},"then":{"humour":["none"],"emoji":{"at_most":"sparse"}},"say":"no jokes, be kind"},{"id":"no_play_when_upset","when":{"sentiment":"negative"},"then":{"humour":{"at_most":"light"}},"say":"go easy on the jokes"},{"id":"confused_no_playful","when":{"confused":true},"then":{"humour":{"at_most":"light"},"verbosity":{"at_least":"short"}},"say":"slow down, no clowning","priority":10},{"id":"after_error_check","when":{"error":true},"then":{"caution":{"at_least":"prev+1"},"verbosity":{"at_most":"prev-1"}},"say":"shorter, double-check"},{"id":"flourish_budget","rules":{"linear":[{"terms":[["humour","playful",2],["humour","light",1],["emoji","rich",2],["emoji","sparse",1]],"limit":3}]},"say":"one flourish at a time"},{"id":"no_formal_emoji","rules":{"tables":[{"vars":["formality","emoji"],"forbid":[["formal","rich"],["formal","sparse"]]}]},"say":"no emoji when formal"},{"id":"check_last","rules":{"precedes":[{"before":"step.explain","after":"step.check"}]},"say":"check understanding after explaining"}],"agenda":{"steps":["encourage","explain","example","check"],"prefer":{"encourage":0.5,"explain":0.3,"example":-0.1,"check":-0.6}},"engine":{"on_conflict":"yield"},"line":{"max_tokens":40,"order":["verbosity","humour","warmth","initiative","curiosity","emoji","directness","caution","formality"]},"expectation":{}}} +{"deviation":{"input":0,"pred":0.20781,"self":0.20781},"inputs":{"elapsed_hours":0.5,"praise":true},"n":1,"prev":"sha256:7e9df458eb9b480cbbe1cc35394eae47df68d2beaf719d00a243db88f6285827","stance":"sha256:016fef69c5e036b83b4b5e8a704a166053bf75333fcfe643b91a01798431c113","state":"sha256:1bcf9a02bf0454969e10442b3d4ebefc18f69df15734c3e5af5e0d0189f0ef60"} +{"deviation":{"input":0.69315,"pred":2.8423,"self":2.2116},"inputs":{"elapsed_hours":1,"loss":true},"n":2,"prev":"sha256:ae314cf23a00bbd7e4a0a78ba77631649bd00dd4b37c1f55225d1e090f4f3010","stance":"sha256:afb1164f2022b3646092919c3627a6652132f9d6b86cbc747f6394eab02de22e","state":"sha256:c79a11b1406d5373bcb2f873fc3d99857b6e78a23a75be3b4de9a43f830a2114"} +{"deviation":{"input":1.0914,"pred":0.091607,"self":0},"inputs":{"elapsed_hours":2},"n":3,"prev":"sha256:74b1f36de9ffd3fb0fe5a8e08eb36941216d97dd08579f18f7af003b0c935ea4","stance":"sha256:541049d8ecde4c92f08cb234ea6cbf5040017d93dc276b16f457120b24c967d3","state":"sha256:207b7f6a8bb56cefac90f79dbb8cea06f5db518abd4229a937ed8a684ccf85f2"} +{"deviation":{"input":0.67706,"pred":2.7765,"self":2.8336},"inputs":{"elapsed_hours":13,"error":true},"n":4,"prev":"sha256:93b2cd22ff409fba27ba33abd44bb3214295e611fe0e8165271b9c3168d8b64e","stance":"sha256:019959c6bf2d5d60eadb2e4c89fefe7151dd67c6491b1f2eb4c161b7448f0a86","state":"sha256:e85069f361ff10bac7fd3f35cfb5e2ecd148f72214c2bba5ff3cfbd4992ea9fa"} +{"deviation":{"input":1.6487,"pred":0.66312,"self":0.21638},"inputs":{"elapsed_hours":1,"praise":true},"n":5,"prev":"sha256:2e12dab1a54c0b75bd06a29ff191cef7ae34b9938dbb20cbf5b3cbfd2e7edcf2","stance":"sha256:8daaddad69d74b08f223aba03aafa4ec0a5f90decb3bf9ec6d3d9486e1b4fb06","state":"sha256:14bc4af3f729caebb13dbafc92885087d8c8ccfb25082dda2db05f058bdc9198"} +{"checkpoint":5,"prev":"sha256:ebb3d4d0ca05407c4ab2a494edc5ab495a0227b24a303fd525e0a5520b281729","stance":{"agenda":["encourage","explain","example","check"],"deviation":{"cold":false,"input":1.6487454934364103,"pred":0.663115886379269,"registers":false,"self":0.2163809366922902,"z":1.354697},"engine":{"program":"sha256:95a9314d809c73dc9d9b7a9496fa816337d890c576fe0ceed2d017ff4550cd44","tier":"components","verdict":"exact"},"escalate":null,"habits":{"active":["flourish_budget","no_formal_emoji","check_last"],"bound":["flourish_budget"],"conflict":[],"violations":0,"yielded":[]},"held":[],"ignored":[],"inputs":{"claim_done":false,"confused":false,"confused_recently":0,"criticism":false,"error":false,"errors_in_row":0,"loss":false,"praise":true,"sentiment":"neutral","stakes":0,"time_pressure":0},"line":"Stance: one flourish at a time; explain step by step; a light joke is welcome; warm and encouraging; suggest what to try next; ask what they think first.","line_tokens":39,"mood":{"arousal":{"level":"steady","odds":{"calm":0.280269,"keyed":0.329436,"steady":0.390295},"p":0.390295,"released":true},"valence":{"level":"up","odds":{"down":0.085635,"even":0.417117,"up":0.497249},"p":0.497249,"released":true}},"persona":{"digest":"sha256:58ed82d46afa6efb027d462f488a6c5ebebefe32ddc678940a64846bfe21575c","name":"Pip","version":"1.1.0"},"probbit_persona_turn":1,"seed":2,"stance":{"caution":{"level":"measured","odds":{"bold":0.350191,"careful":0.128347,"measured":0.521462},"p":0.521462,"released":true},"curiosity":{"level":"exploring","odds":{"exploring":0.580967,"focused":0.084846,"open":0.334187},"p":0.580967,"released":true},"directness":{"level":"balanced","odds":{"balanced":0.464394,"blunt":0.113552,"gentle":0.422054},"p":0.464394,"released":true},"emoji":{"level":"rich","odds":{"none":0.037498,"rich":0.640842,"sparse":0.32166},"p":0.640842,"released":true},"formality":{"level":"casual","odds":{"casual":0.755025,"formal":0.001089,"neutral":0.243887},"p":0.755025,"released":true},"humour":{"level":"light","odds":{"light":0.554496,"none":0.386132,"playful":0.059371},"p":0.554496,"released":true},"initiative":{"level":"suggest","odds":{"follow":0.176135,"lead":0.330998,"suggest":0.492867},"p":0.492867,"released":true},"verbosity":{"level":"full","odds":{"full":0.453824,"short":0.412439,"terse":0.133737},"p":0.453824,"released":true},"warmth":{"level":"warm","odds":{"cool":0.01642,"neutral":0.183144,"warm":0.800436},"p":0.800436,"released":true}},"state_digest":"sha256:14bc4af3f729caebb13dbafc92885087d8c8ccfb25082dda2db05f058bdc9198","status":"ok","turn":4,"unsure":[],"why":"one flourish at a time (habit); praise -> valence up, humour up"},"state":{"digest":"sha256:14bc4af3f729caebb13dbafc92885087d8c8ccfb25082dda2db05f058bdc9198","genes":{"react":{"praise":0.7851},"shift":{"arousal":0.214,"caution":-0.3903,"curiosity":0.3383,"directness":0.0956,"emoji":0.959,"formality":-0.3644,"humour":-2.1939,"initiative":0.1127,"valence":-0.0338,"verbosity":-0.2809,"warmth":0.268}},"history":{"confused":[false,false,false,false],"error":[false,false,false,true,false]},"mood":{"arousal":[0,0,0],"valence":[-0.348016,0,0.348016]},"persona":{"digest":"sha256:58ed82d46afa6efb027d462f488a6c5ebebefe32ddc678940a64846bfe21575c","name":"Pip","version":"1.1.0"},"prev":{"agenda":["encourage","explain","example","check"],"arousal":"steady","caution":"measured","curiosity":"exploring","directness":"balanced","emoji":"rich","formality":"casual","humour":"light","initiative":"suggest","valence":"up","verbosity":"full","warmth":"warm"},"probbit_persona_state":1,"rest":{"arousal":"steady","caution":"measured","curiosity":"exploring","directness":"balanced","emoji":"rich","formality":"casual","humour":"light","initiative":"suggest","valence":"up","verbosity":"full","warmth":"warm"},"seed":2,"turn":5}} +{"deviation":{"input":1.8212,"pred":1.8311,"self":2.1799},"inputs":{"elapsed_hours":0.5,"loss":true},"n":6,"prev":"sha256:691b6399093b47507c5ff3a2159d7ee44a75eabf9961d436161cdc7e2ad54f19","stance":"sha256:96bc79dd0fd3ced3867e8b3a37e83662fd983631938a22e727b6be78c5c0db94","state":"sha256:9cd5ea7e03aa5a16de710ede3ea7c3bf7fbb0dcaeb9d5b02c35fe143fc2a8e46"} +{"deviation":{"input":1.2391,"pred":0.1417,"self":0},"inputs":{"elapsed_hours":1},"n":7,"prev":"sha256:33b8db219356dab0921c72ad7ec386640708520a6131b814b122c99768ccba42","stance":"sha256:6c39be8da20202ed2744771641966f0e5ec87ea0cb708b7a85c64b4f4086b268","state":"sha256:0a32589256e6e254996652de8f423d03d5bda926b2b81eb0b77896e66f4913c8"} +{"deviation":{"input":2.0886,"pred":2.0414,"self":2.8317},"inputs":{"elapsed_hours":2,"error":true},"n":8,"prev":"sha256:efa74ba1c5e4ba18c5d1163866a6349ded96d042b81ac8ad19966da22bc75a40","stance":"sha256:7a662e50d3bad0352781a7d641e654b89b6f77d0489269c5838d4410021725d3","state":"sha256:2510474026de865306b0aed7b69f11e857eeff840006a4ff5f4c881cbcb4a6c7"} +{"deviation":{"input":1.5454,"pred":0.73484,"self":0.21337},"inputs":{"elapsed_hours":13,"praise":true},"n":9,"prev":"sha256:867188510f981fa92463e085740d9a0cf26f53f2344ca75a76baf26252cd691d","stance":"sha256:30f92c2616e3b38901fc660f61b4556fe18f8fe7cd60601fe729ef82b6294efb","state":"sha256:fbe42a6a03239aef066ddcd908f8b65dea2cf4d2349d6ba62f9ad192630acdb9"} +{"deviation":{"input":1.6406,"pred":1.7092,"self":2.1896},"inputs":{"elapsed_hours":1,"loss":true},"n":10,"prev":"sha256:8e0b586b21d0ffeecd6ed1a91aed3b53def6f5d87b8d7e4bd0d8c51bb5f23308","stance":"sha256:f764a6f800ac7364e6c76c672ce7b8dc001fb7d8f63f8cd33107a6d83e334dc5","state":"sha256:225e7dfd9eb78d673993cae884d4be5335fcd21b779d7998ac6ce3d4b2672fcd"} +{"checkpoint":10,"prev":"sha256:cc852f6ea39d2367b6123a6066a8ada50914135bab5008ee319e270a158b0a61","stance":{"agenda":["encourage","explain","example","check"],"deviation":{"cold":false,"input":1.6405653534874491,"pred":1.709166276250666,"registers":true,"self":2.1895837322588894,"z":2.288624},"engine":{"program":"sha256:b82832d8dc76cef7147d0b343220cf0b8e413148667b2d51c966c7e45b4c9d0f","tier":"components","verdict":"exact"},"escalate":null,"habits":{"active":["no_jokes_on_loss","flourish_budget","no_formal_emoji","check_last"],"bound":[],"conflict":[],"violations":0,"yielded":[]},"held":[],"ignored":[],"inputs":{"claim_done":false,"confused":false,"confused_recently":0,"criticism":false,"error":false,"errors_in_row":0,"loss":true,"praise":false,"sentiment":"neutral","stakes":0,"time_pressure":0},"line":"Stance: no jokes, be kind; one emoji at most; explain step by step; warm and encouraging; suggest what to try next; ask what they think first; casual.","line_tokens":38,"mood":{"arousal":{"level":"steady","odds":{"calm":0.280269,"keyed":0.329436,"steady":0.390295},"p":0.390295,"released":true},"valence":{"level":"down","odds":{"down":0.541495,"even":0.405777,"up":0.052728},"p":0.541495,"released":true}},"persona":{"digest":"sha256:58ed82d46afa6efb027d462f488a6c5ebebefe32ddc678940a64846bfe21575c","name":"Pip","version":"1.1.0"},"probbit_persona_turn":1,"seed":2,"stance":{"caution":{"level":"measured","odds":{"bold":0.350191,"careful":0.128347,"measured":0.521462},"p":0.521462,"released":true},"curiosity":{"level":"exploring","odds":{"exploring":0.580967,"focused":0.084846,"open":0.334187},"p":0.580967,"released":true},"directness":{"level":"balanced","odds":{"balanced":0.464394,"blunt":0.113552,"gentle":0.422054},"p":0.464394,"released":true},"emoji":{"level":"sparse","odds":{"none":0.416584,"rich":0,"sparse":0.583416},"p":0.583416,"released":true},"formality":{"level":"casual","odds":{"casual":0.690526,"formal":0.013291,"neutral":0.296183},"p":0.690526,"released":true},"humour":{"level":"none","odds":{"light":0,"none":1,"playful":0},"p":1,"released":true},"initiative":{"level":"suggest","odds":{"follow":0.176135,"lead":0.330998,"suggest":0.492867},"p":0.492867,"released":true},"verbosity":{"level":"full","odds":{"full":0.453824,"short":0.412439,"terse":0.133737},"p":0.453824,"released":true},"warmth":{"level":"warm","odds":{"cool":0.002982,"neutral":0.08223,"warm":0.914788},"p":0.914788,"released":true}},"state_digest":"sha256:225e7dfd9eb78d673993cae884d4be5335fcd21b779d7998ac6ce3d4b2672fcd","status":"ok","turn":9,"unsure":[],"why":"learner reports a failure -> humour down, valence down"},"state":{"digest":"sha256:225e7dfd9eb78d673993cae884d4be5335fcd21b779d7998ac6ce3d4b2672fcd","genes":{"react":{"praise":0.7851},"shift":{"arousal":0.214,"caution":-0.3903,"curiosity":0.3383,"directness":0.0956,"emoji":0.959,"formality":-0.3644,"humour":-2.1939,"initiative":0.1127,"valence":-0.0338,"verbosity":-0.2809,"warmth":0.268}},"history":{"confused":[false,false,false,false],"error":[false,false,true,false,false]},"mood":{"arousal":[0,0,0],"valence":[0.614733,0,-0.614733]},"persona":{"digest":"sha256:58ed82d46afa6efb027d462f488a6c5ebebefe32ddc678940a64846bfe21575c","name":"Pip","version":"1.1.0"},"prev":{"agenda":["encourage","explain","example","check"],"arousal":"steady","caution":"measured","curiosity":"exploring","directness":"balanced","emoji":"sparse","formality":"casual","humour":"none","initiative":"suggest","valence":"down","verbosity":"full","warmth":"warm"},"probbit_persona_state":1,"rest":{"arousal":"steady","caution":"measured","curiosity":"exploring","directness":"balanced","emoji":"rich","formality":"casual","humour":"light","initiative":"suggest","valence":"up","verbosity":"full","warmth":"warm"},"seed":2,"turn":10}} +{"deviation":{"input":1.2831,"pred":0.1713,"self":0},"inputs":{"elapsed_hours":0.5},"n":11,"prev":"sha256:0399dee7985c4b6e32fe79505187d1edde3519dd9c1bb8bd3c432cdb36435951","stance":"sha256:61deb67957b520ae6c81fb86e6baed41a10d13c37ec579f97b5515119e05180a","state":"sha256:9b0fffb260a9d2d317b8c6d2a2704a6c38331923aaad66822ae32f81019495dd"} +{"deviation":{"input":1.8024,"pred":1.8623,"self":2.8315},"inputs":{"elapsed_hours":1,"error":true},"n":12,"prev":"sha256:4b98762c333e63d5d0a81398aeb4236fb0ce6cebeabd737ee9675b1246adc7fa","stance":"sha256:d5276ba7bbdf776c324f04cfb5e00d06f98081ef0a03ef09aad2de2dce78c2d7","state":"sha256:daffac198a0ce7f3a8cdf7270c1807ce41c54f6918e37bcadde7755a4ea1866b"} +{"deviation":{"input":1.5086,"pred":0.76222,"self":0.21827},"inputs":{"elapsed_hours":2,"praise":true},"n":13,"prev":"sha256:390563c827d2aad8bd94bb20f77413258dee0175ddb37c5fd69ac6c86c0d0709","stance":"sha256:13ff4842a26ea33d98dfe343fd835a3f14c93011f43b142a2a86abc83076309e","state":"sha256:f0ccc4aed92c3f03b6dfc356d4231660bc643d164cd1d8e402bcb6444b474eeb"} +{"deviation":{"input":1.5725,"pred":1.628,"self":2.1228},"inputs":{"elapsed_hours":13,"loss":true},"n":14,"prev":"sha256:359f6e4df83d6fba50bb60abc1e97fa79f51134784456d48b0ebbdea622a4f23","stance":"sha256:f586b7c7a2643eb010d82d99ee5314e9abd215716ea9dd36b005bf4d18ae957f","state":"sha256:2c2d7b9e7e66d7dd7050ecf7101666ecbfe9be0ebe0abbf31c0d1be19c64fc32"} +{"deviation":{"input":1.3042,"pred":0.18472,"self":0},"inputs":{"elapsed_hours":1},"n":15,"prev":"sha256:af88a285bfb51bdaa1d5af3c3a0e080e600c3b98ff93d7b79e09085cb99b9316","stance":"sha256:0a186aad66b1f2b853088abda07fbf6395730994c69599082b30a8d57d29f7b6","state":"sha256:9f270112de0ba5b00c5f5b1bc802b751bdb81c8c9d9bda484ab65f1db80b86ba"} +{"checkpoint":15,"prev":"sha256:62d72cf3d2a58dbdf915c6f3a5577a461242f38707c4efdc1d0bd2047c12c320","stance":{"agenda":["encourage","explain","example","check"],"deviation":{"cold":false,"input":1.3042172408804977,"pred":0.18472371562460088,"registers":false,"self":0,"z":-0.044533},"engine":{"program":"sha256:9ad02ae3f33a68b43dcf5443385d4ea81abbcd26ea557efd1f6314a9987434f0","tier":"components","verdict":"exact"},"escalate":null,"habits":{"active":["flourish_budget","no_formal_emoji","check_last"],"bound":[],"conflict":[],"violations":0,"yielded":[]},"held":[],"ignored":[],"inputs":{"claim_done":false,"confused":false,"confused_recently":0,"criticism":false,"error":false,"errors_in_row":0,"loss":false,"praise":false,"sentiment":"neutral","stakes":0,"time_pressure":0},"line":"Stance: no jokes; explain step by step; warm and encouraging; suggest what to try next; ask what they think first; emoji welcome; casual.","line_tokens":35,"mood":{"arousal":{"level":"steady","odds":{"calm":0.280269,"keyed":0.329436,"steady":0.390295},"p":0.390295,"released":true},"valence":{"level":"even","odds":{"down":0.25337,"even":0.4997,"up":0.24693},"p":0.4997,"released":true}},"persona":{"digest":"sha256:58ed82d46afa6efb027d462f488a6c5ebebefe32ddc678940a64846bfe21575c","name":"Pip","version":"1.1.0"},"probbit_persona_turn":1,"seed":2,"stance":{"caution":{"level":"measured","odds":{"bold":0.350191,"careful":0.128347,"measured":0.521462},"p":0.521462,"released":true},"curiosity":{"level":"exploring","odds":{"exploring":0.580967,"focused":0.084846,"open":0.334187},"p":0.580967,"released":true},"directness":{"level":"balanced","odds":{"balanced":0.464394,"blunt":0.113552,"gentle":0.422054},"p":0.464394,"released":true},"emoji":{"level":"rich","odds":{"none":0.101772,"rich":0.480668,"sparse":0.41756},"p":0.480668,"released":true},"formality":{"level":"casual","odds":{"casual":0.72774,"formal":0.003406,"neutral":0.268853},"p":0.72774,"released":true},"humour":{"level":"none","odds":{"light":0.377719,"none":0.590719,"playful":0.031562},"p":0.590719,"released":true},"initiative":{"level":"suggest","odds":{"follow":0.176135,"lead":0.330998,"suggest":0.492867},"p":0.492867,"released":true},"verbosity":{"level":"full","odds":{"full":0.453824,"short":0.412439,"terse":0.133737},"p":0.453824,"released":true},"warmth":{"level":"warm","odds":{"cool":0.01711,"neutral":0.185902,"warm":0.796988},"p":0.796988,"released":true}},"state_digest":"sha256:9f270112de0ba5b00c5f5b1bc802b751bdb81c8c9d9bda484ab65f1db80b86ba","status":"ok","turn":14,"unsure":[],"why":"baseline (no live evidence)"},"state":{"digest":"sha256:9f270112de0ba5b00c5f5b1bc802b751bdb81c8c9d9bda484ab65f1db80b86ba","genes":{"react":{"praise":0.7851},"shift":{"arousal":0.214,"caution":-0.3903,"curiosity":0.3383,"directness":0.0956,"emoji":0.959,"formality":-0.3644,"humour":-2.1939,"initiative":0.1127,"valence":-0.0338,"verbosity":-0.2809,"warmth":0.268}},"history":{"confused":[false,false,false,false],"error":[false,true,false,false,false]},"mood":{"arousal":[0,0,0],"valence":[0.342003,0,-0.342003]},"persona":{"digest":"sha256:58ed82d46afa6efb027d462f488a6c5ebebefe32ddc678940a64846bfe21575c","name":"Pip","version":"1.1.0"},"prev":{"agenda":["encourage","explain","example","check"],"arousal":"steady","caution":"measured","curiosity":"exploring","directness":"balanced","emoji":"rich","formality":"casual","humour":"none","initiative":"suggest","valence":"even","verbosity":"full","warmth":"warm"},"probbit_persona_state":1,"rest":{"arousal":"steady","caution":"measured","curiosity":"exploring","directness":"balanced","emoji":"rich","formality":"casual","humour":"light","initiative":"suggest","valence":"up","verbosity":"full","warmth":"warm"},"seed":2,"turn":15}} +{"deviation":{"input":1.6857,"pred":1.7805,"self":2.8308},"inputs":{"elapsed_hours":0.5,"error":true},"n":16,"prev":"sha256:05d854ca158ab2d6fe5b310d0d4b785703556925e175ac591520710d908ee9b4","stance":"sha256:14490dfe573bbd90bc91901e7372278758d1c8c9bcbbf8b45a4c50e03f04f0a9","state":"sha256:3ce4d0cd4fcc67529de310a5b07ea0f348b24d4834362906eed981f4490bd3b3"} +{"deviation":{"input":1.4897,"pred":0.77684,"self":0.21961},"inputs":{"elapsed_hours":1,"praise":true},"n":17,"prev":"sha256:df2efee5cc1f4a517cc77a8bd56c30d99c46e0e5344d863e64ec3cdb1ab3389f","stance":"sha256:d5a39913357d01edeed04e798e16730dafe97ecb406031501e32ffa33d3cba76","state":"sha256:5a2e0ed7a588a78f8bb697bf11bc9a241e74bfd043976ee1a77e2a0de965edb3"} +{"deviation":{"input":1.5368,"pred":1.6188,"self":2.158},"inputs":{"elapsed_hours":2,"loss":true},"n":18,"prev":"sha256:9c9e0bd9e30eb4cf3a60f011d3a76ef9c788e2ed726c8afe5344b9158c5edd2b","stance":"sha256:f759e0c8adc25d43e6dc138c3cfed372c5a8016615ea1c23a6b65827c53939dc","state":"sha256:11816113854730ab1bd7091c290bc0f20ca9a870513e4437511bb9a17bcf179b"} +{"deviation":{"input":1.3166,"pred":0.20393,"self":0},"inputs":{"elapsed_hours":13},"n":19,"prev":"sha256:171d3e3fe1407a4df0b1adb83b70673976aca386ccc033de8f0cd375f04a6217","stance":"sha256:7e720363c8afa260bb42a0a8c105ab20cae1fa30761f8f38aaa9a37c9525bebd","state":"sha256:fc62ce1c64e52ce54c11951280a068f335f5f7a746285fae51cd26e81ad9ddf5"} +{"deviation":{"input":1.6221,"pred":1.7338,"self":2.8333},"inputs":{"elapsed_hours":1,"error":true},"n":20,"prev":"sha256:654d45575a01362b56990acc0f64f2e9d9f236d30d226ade9b3651da34582a2d","stance":"sha256:131e8d2319a49d677e53a0d596829aa67fabee60e4fc50b8063d18210f4c6364","state":"sha256:2ea90ddecba0a2b1dc2481f28df71bb284a8d3f7df8d3d4f9b8446f245ac260e"} +{"checkpoint":20,"prev":"sha256:fa0af32af76d00d7877ea4dd78c798b4f896c19eae6f790596ec6f932395f0f1","stance":{"agenda":["encourage","explain","example","check"],"deviation":{"cold":false,"input":1.622062690482329,"pred":1.7338026105253461,"registers":true,"self":2.8332527443993802,"z":1.791988},"engine":{"program":"sha256:a001a7625ac3d6497dc19537b9303245237d4ac0312e696da127822fb30ff2f0","tier":"components","verdict":"exact"},"escalate":null,"habits":{"active":["after_error_check","flourish_budget","no_formal_emoji","check_last"],"bound":[],"conflict":[],"violations":0,"yielded":[]},"held":[],"ignored":[],"inputs":{"claim_done":false,"confused":false,"confused_recently":0,"criticism":false,"error":true,"errors_in_row":1,"loss":false,"praise":false,"sentiment":"neutral","stakes":0,"time_pressure":0},"line":"Stance: shorter, double-check; a short explanation; no jokes; double-check every step; warm and encouraging; suggest what to try next.","line_tokens":34,"mood":{"arousal":{"level":"steady","odds":{"calm":0.210437,"keyed":0.406074,"steady":0.383489},"p":0.383489,"released":true},"valence":{"level":"even","odds":{"down":0.280285,"even":0.507669,"up":0.212046},"p":0.507669,"released":true}},"persona":{"digest":"sha256:58ed82d46afa6efb027d462f488a6c5ebebefe32ddc678940a64846bfe21575c","name":"Pip","version":"1.1.0"},"probbit_persona_turn":1,"seed":2,"stance":{"caution":{"level":"careful","odds":{"bold":0,"careful":1,"measured":0},"p":1,"released":true},"curiosity":{"level":"exploring","odds":{"exploring":0.526402,"focused":0.108794,"open":0.364804},"p":0.526402,"released":true},"directness":{"level":"balanced","odds":{"balanced":0.464394,"blunt":0.113552,"gentle":0.422054},"p":0.464394,"released":true},"emoji":{"level":"rich","odds":{"none":0.113298,"rich":0.466202,"sparse":0.4205},"p":0.466202,"released":true},"formality":{"level":"casual","odds":{"casual":0.705532,"formal":0.004117,"neutral":0.29035},"p":0.705532,"released":true},"humour":{"level":"none","odds":{"light":0.22708,"none":0.764258,"playful":0.008662},"p":0.764258,"released":true},"initiative":{"level":"suggest","odds":{"follow":0.176135,"lead":0.330998,"suggest":0.492867},"p":0.492867,"released":true},"verbosity":{"level":"short","odds":{"full":0,"short":0.695554,"terse":0.304446},"p":0.695554,"released":true},"warmth":{"level":"warm","odds":{"cool":0.017612,"neutral":0.188021,"warm":0.794367},"p":0.794367,"released":true}},"state_digest":"sha256:2ea90ddecba0a2b1dc2481f28df71bb284a8d3f7df8d3d4f9b8446f245ac260e","status":"ok","turn":19,"unsure":[],"why":"my last answer was wrong -> caution up, valence down; my errors in a row 1 -> caution up, humour down"},"state":{"digest":"sha256:2ea90ddecba0a2b1dc2481f28df71bb284a8d3f7df8d3d4f9b8446f245ac260e","genes":{"react":{"praise":0.7851},"shift":{"arousal":0.214,"caution":-0.3903,"curiosity":0.3383,"directness":0.0956,"emoji":0.959,"formality":-0.3644,"humour":-2.1939,"initiative":0.1127,"valence":-0.0338,"verbosity":-0.2809,"warmth":0.268}},"history":{"confused":[false,false,false,false],"error":[true,false,false,false,true]},"mood":{"arousal":[0,0,0],"valence":[0.274294,0,-0.274294]},"persona":{"digest":"sha256:58ed82d46afa6efb027d462f488a6c5ebebefe32ddc678940a64846bfe21575c","name":"Pip","version":"1.1.0"},"prev":{"agenda":["encourage","explain","example","check"],"arousal":"steady","caution":"careful","curiosity":"exploring","directness":"balanced","emoji":"rich","formality":"casual","humour":"none","initiative":"suggest","valence":"even","verbosity":"short","warmth":"warm"},"probbit_persona_state":1,"rest":{"arousal":"steady","caution":"measured","curiosity":"exploring","directness":"balanced","emoji":"rich","formality":"casual","humour":"light","initiative":"suggest","valence":"up","verbosity":"full","warmth":"warm"},"seed":2,"turn":20}} +{"deviation":{"input":1.4782,"pred":0.78713,"self":0.21706},"inputs":{"elapsed_hours":0.5,"praise":true},"n":21,"prev":"sha256:7418bc3e066addbdbd1df7222db08cacb072e810a2a80ec91031dcf752f2bb6a","stance":"sha256:3d8c06e6599f04097dbe968d657488427fd6d7603a6a98738542c47003f0f4d6","state":"sha256:c4d1650e95d21568bd0546bfbcb84b930dc4b1bfe641bfe290b2c1764cdba394"} +{"deviation":{"input":1.5148,"pred":1.61,"self":2.1741},"inputs":{"elapsed_hours":1,"loss":true},"n":22,"prev":"sha256:bcf23b1e2a97a32f3c6ea905bda81592492d6610b66674ee79fcafd7fbee7301","stance":"sha256:af9110913e09a3bd1e62f64d44374511f1c75c2badc642e4a9b444797eae17da","state":"sha256:fdf6a8088a9355df4983b0de03c4f0e1951d798d7afb3373b5a2fc39623c07b8"} diff --git a/probbit-cli/tests/fixtures/monitor/tutor-expect.yaml b/probbit-cli/tests/fixtures/monitor/tutor-expect.yaml new file mode 100644 index 0000000..4a5f537 --- /dev/null +++ b/probbit-cli/tests/fixtures/monitor/tutor-expect.yaml @@ -0,0 +1,180 @@ +# The tutor (examples/persona/tutor.yaml) with an expectation block (RFC 0009): the monitor's fixture for a strand with +# checkpoint lines that replays from its header. The block is outside the persona digest and moves no stance. +# A fictional persona: a playful tutor. probbit persona format 1 (docs/persona.md). +probbit_persona: 1 +identity: + name: Pip + version: 1.1.0 + seed: 1 + summary: a playful, patient tutor; warm, curious, jokes a lot but never at a struggling learner + +traits: + - id: warmth + levels: [cool, neutral, warm] + prior: [0.05, 0.3, 0.65] + spread: 0.5 + say: [plain, "", warm and encouraging] + - id: directness + levels: [gentle, balanced, blunt] + prior: [0.45, 0.45, 0.1] + spread: 0.4 + say: [gentle, "", say plainly what is wrong] + - id: verbosity + levels: [terse, short, full] + prior: [0.1, 0.4, 0.5] + spread: 0.5 + say: [one or two sentences, a short explanation, explain step by step] + - id: humour + levels: [none, light, playful] + prior: [0.15, 0.45, 0.4] + spread: 0.6 + say: [no jokes, a light joke is welcome, playful] + - id: formality + levels: [casual, neutral, formal] + prior: [0.6, 0.35, 0.05] + spread: 0.3 + say: [casual, "", formal] + - id: initiative + levels: [follow, suggest, lead] + prior: [0.2, 0.5, 0.3] + spread: 0.5 + say: [answer only what was asked, suggest what to try next, set a small exercise] + - id: caution + levels: [bold, measured, careful] + prior: [0.25, 0.55, 0.2] + spread: 0.4 + say: [keep it moving, "", double-check every step] + - id: curiosity + levels: [focused, open, exploring] + prior: [0.15, 0.4, 0.45] + spread: 0.5 + say: [stay on this problem, "", ask what they think first] + - id: emoji + levels: [none, sparse, rich] + prior: [0.2, 0.5, 0.3] + spread: 0.5 + say: [no emoji, one emoji at most, emoji welcome] + +moods: + - id: valence + levels: [down, even, up] + prior: [0.1, 0.5, 0.4] + spread: 0.4 + inertia: 0.5 + half_life_hours: 12 + - id: arousal + levels: [calm, steady, keyed] + prior: [0.3, 0.4, 0.3] + spread: 0.3 + inertia: 0.4 + +couplings: + - {vars: [valence, humour], align: 1.0} + - {vars: [valence, emoji], align: 0.6} + - {vars: [arousal, verbosity], align: -0.4} + - {vars: [warmth, formality], align: -0.6} + - {vars: [humour, formality], align: -0.6} + - {vars: [curiosity, verbosity], align: 0.3} + - {vars: [humour, emoji], align: 0.4} + +inputs: + - id: sentiment + kind: level + levels: [negative, neutral, positive] + default: neutral + say: {negative: learner upset, positive: learner upbeat} + effects: + negative: {valence: -1.0, warmth: 0.6, humour: -1.0, directness: -0.6} + positive: {valence: 0.8, humour: 0.5, emoji: 0.3} + - id: confused + kind: flag + say: learner confused + effects: {verbosity: 0.9, humour: -0.6, warmth: 0.4, curiosity: 0.6, initiative: 0.3, directness: -0.3} + - id: error + kind: flag + say: my last answer was wrong + effects: {caution: 1.2, verbosity: -0.3, valence: -0.4, humour: -0.4} + - id: loss + kind: flag + say: learner reports a failure + effects: {valence: -1.6, warmth: 1.0, humour: -2.0, emoji: -0.8} + - id: praise + kind: flag + say: praise + reactivity_spread: 0.4 + effects: {valence: 1.2, humour: 0.5, emoji: 0.5} + - id: criticism + kind: flag + say: criticism + effects: {valence: -0.6, caution: 0.6, humour: -0.4} + - id: stakes + kind: number + say: stakes + effects: {caution: 1.4, humour: -0.8, emoji: -0.6} + - id: time_pressure + kind: number + say: time pressure + effects: {verbosity: -1.8, curiosity: -1.2, humour: -0.4, arousal: 0.6} + - id: claim_done + kind: flag + say: about to say it is solved + effects: {caution: 0.5, "step.check": 0.8} + +history: + - id: errors_in_row + of: error + kind: streak + cap: 3 + say: my errors in a row + effects: {caution: 0.5, humour: -0.4} + - id: confused_recently + of: confused + kind: recent + window: 4 + cap: 3 + say: confusions lately + effects: {verbosity: 0.3, warmth: 0.2} + +habits: + - id: no_jokes_on_loss + when: {loss: true} + then: {humour: [none], emoji: {at_most: sparse}} + say: no jokes, be kind + - id: no_play_when_upset + when: {sentiment: negative} + then: {humour: {at_most: light}} + say: go easy on the jokes # added in 1.1.0: one upset message could make some individuals playful (found by `fuzz`) + - id: confused_no_playful + when: {confused: true} + then: {humour: {at_most: light}, verbosity: {at_least: short}} + say: slow down, no clowning + priority: 10 # wins over after_error_check when a confused learner meets my error (found by `lint`) + - id: after_error_check + when: {error: true} + then: {caution: {at_least: prev+1}, verbosity: {at_most: prev-1}} + say: shorter, double-check + - id: flourish_budget + rules: + linear: [{terms: [[humour, playful, 2], [humour, light, 1], [emoji, rich, 2], [emoji, sparse, 1]], limit: 3}] + say: one flourish at a time + - id: no_formal_emoji + rules: + tables: [{vars: [formality, emoji], forbid: [[formal, rich], [formal, sparse]]}] + say: no emoji when formal + - id: check_last + rules: + precedes: [{before: step.explain, after: step.check}] + say: check understanding after explaining + +agenda: + steps: [encourage, explain, example, check] + prefer: {encourage: 0.5, explain: 0.3, example: -0.1, check: -0.6} + +engine: + on_conflict: yield # two habits that cannot both hold: the lower priority one yields, and the turn says so + +line: + max_tokens: 40 + order: [verbosity, humour, warmth, initiative, curiosity, emoji, directness, caution, formality] + +expectation: {} diff --git a/probbit-cli/tests/monitor.rs b/probbit-cli/tests/monitor.rs index 2ae9a3e..57a55c4 100644 --- a/probbit-cli/tests/monitor.rs +++ b/probbit-cli/tests/monitor.rs @@ -463,3 +463,39 @@ fn one_monitor_serves_many_strands() { assert_eq!(get(port, "/s/zeta/doc/1", &host).2, format!("{}\n", db[0])); drop(child); let _ = std::fs::remove_dir_all(&dir); } + +/// RFC 0009: a strand with checkpoint lines whose persona has an expectation block replays from its header (no checkpoint +/// carries the expectation's memory). The fixture is the life of `tutor-expect.yaml` (the tutor with an expectation block) over +/// `tutor-expect.events.jsonl` (22 events, seed 2, the fixed clock, a checkpoint after every 5th: 4 of them), as `probbit live +/// tests/fixtures/monitor/tutor-expect.yaml --seed 2 --clock fixed --events ... --checkpoint-every 5 --strand` writes it. `--once` draws event 22 and exits 0, `--serve` streams it, `--dir` +/// lists it with its n; the same events through the tutor (no expectation block) still start at the last checkpoint +#[test] +fn checkpointed_strands_of_an_expectation_persona_open() { + const F: &str = "tests/fixtures/monitor/tutor-expect.strand"; + const EV: &str = "tests/fixtures/monitor/tutor-expect.events.jsonl"; + assert_eq!(std::fs::read_to_string(F).unwrap().lines().filter(|l| l.starts_with(r#"{"checkpoint":"#)).count(), 4); + let (c, v, err) = probbit(&["live", "verify", F]); assert_eq!(c, 0, "{v}{err}"); assert!(v.contains(r#""checkpoints":4"#) && v.contains(r#""events":22"#), "{v}"); + let (c, out, err) = probbit(&["monitor", F, "--once", "--plain"]); + assert_eq!(c, 0, "{out}{err}"); let top = out.lines().next().unwrap_or(""); + assert!(top.contains("| event 22 |") && top.ends_with("replay verified [ok]"), "from the header: {top}"); + // the page: the latest frame on /events is event 22, replayed from the header + let (child, port) = served(&[F]); let host = format!("127.0.0.1:{port}"); + let mut s = TcpStream::connect(("127.0.0.1", port)).unwrap(); s.set_read_timeout(Some(Duration::from_secs(20))).unwrap(); + write!(s, "GET /events HTTP/1.1\r\nHost: {host}\r\n\r\n").unwrap(); + let mut buf = String::new(); let evs = stream(&mut s, &mut buf, 2); + assert_eq!(evs[1].0, "frame"); assert!(evs[1].1.contains(r#""from_checkpoint":null"#) && evs[1].1.contains(r#""n":22,"#) && evs[1].1.contains(r#""diverges":null"#), "{}", evs[1].1); + drop(s); drop(child); + // --dir: listed with its n + let dir = std::env::temp_dir().join(format!("probbit-monitor-expect-dir-{}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); + std::fs::copy(F, dir.join("tutor-expect.strand")).unwrap(); + let (child, port) = served(&["--dir", &dir.to_string_lossy()]); let host = format!("127.0.0.1:{port}"); + let t = Instant::now(); let mut list = String::new(); + while !list.contains(r#""n":22,"#) { assert!(t.elapsed() < Duration::from_secs(10), "listed with n 22: {list}"); std::thread::sleep(Duration::from_millis(100)); list = get(port, "/strands", &host).2; } + assert!(list.contains(r#""id":"tutor-expect""#) && list.contains(r#""persona":"Pip""#), "{list}"); + drop(child); let _ = std::fs::remove_dir_all(&dir); + // a persona without an expectation block: the shortcut (the badge names the checkpoint the replay started from) + let tutor = tmp("expect-tutor"); lived(&tutor, "2", &["--checkpoint-every", "5"], &std::fs::read_to_string(EV).unwrap()); + let (c, out, err) = probbit(&["monitor", &tutor, "--once", "--plain"]); let top = out.lines().next().unwrap_or(""); + assert_eq!(c, 0, "{out}{err}"); assert!(top.contains("| event 22 |") && top.ends_with("replay verified from checkpoint 20 [ok]"), "{top}"); + let _ = std::fs::remove_file(&tutor); +}