yog 0.0.1

yog: a balls-oriented session manager for lernie loops (egui frontend)
Documentation
//! §4.4 terminal reading rules for a settled `response.json`.
//!
//! Once the fd is closed (the classifier reaches here only with no lock
//! held, §3.5), the latest step's `response.json` is classified by its tail
//! (§4.4):
//!
//! - *complete* — last line `end`, and the last segment carries a `finish`
//!   with no `error`.
//! - *failed* — last segment carries an `error` (retry budget exhausted or
//!   non-retryable, §2.10).
//! - *killed* — closed with no trailing `end` (writer died mid-stream,
//!   §2.9).
//!
//! Only *complete* is quiescent; *failed* and *killed* are stopped (§3.5).
//! This is a self-delimiting reader over appended attempt segments (§4.4):
//! only the **last** segment decides, because it is the settled outcome.

/// The §4.4 settled outcome a closed `response.json` tail carries. Derived
/// from the **last** segment alone (the settled outcome), by the same
/// self-delimiting reader the live view uses. Widened to `pub(crate)` (the
/// enum + [`framing`] + [`segment_count`]) so the Y13 steps inspector reuses
/// this classifier instead of re-parsing the JSONL (§15 Y13: "reuse
/// git_tree::terminal's segment classification — do NOT duplicate the
/// parser"); [`last_segment_complete`] stays the live classifier's thin view.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Framing {
    /// Last line `end`, last segment a `finish` with no `error` (§4.4).
    Complete,
    /// Last segment carries an `error` — retry budget exhausted or
    /// non-retryable (§2.10).
    Failed,
    /// Closed with no trailing `end`, empty, or an `end` with no `finish`
    /// — the writer died mid-stream, the step never ran, or a call in
    /// flight right now (kill/crash/in-flight are indistinguishable on
    /// disk, §2.9).
    Killed,
}

/// Classify a `response.json` payload by its tail (§4.4). Only the last
/// segment decides. A trailing partial line (no `\n` yet) is dropped.
pub(crate) fn framing(bytes: &[u8]) -> Framing {
    // Only fully-terminated lines: drop anything after the final newline.
    let terminated = match bytes.iter().rposition(|&b| b == b'\n') {
        // `idx` is a position within `bytes`, so `..=idx` always yields `Some`.
        Some(idx) => bytes.get(..=idx).unwrap_or(bytes),
        None => return Framing::Killed,
    };
    let lines: Vec<&[u8]> = terminated
        .split(|&b| b == b'\n')
        .filter(|l| !l.is_empty())
        .collect();
    let Some((last, rest)) = lines.split_last() else {
        return Framing::Killed;
    };
    if event_type(last) != Some("end") {
        return Framing::Killed;
    }
    // Walk the last segment backward from just before the final `end` to
    // the previous segment's `end` boundary; require a `finish`, no `error`.
    let mut saw_finish = false;
    for line in rest.iter().rev() {
        match event_type(line) {
            Some("end") => break,
            Some("error") => return Framing::Failed,
            Some("finish") => saw_finish = true,
            _ => {}
        }
    }
    if saw_finish {
        Framing::Complete
    } else {
        Framing::Killed
    }
}

/// Number of completed attempt segments — the count of `end` events (§4.4:
/// every attempt segment terminates with an `end`). A still-open final
/// segment (no trailing `end`) is not yet counted; this is the "attempts"
/// figure the steps inspector shows.
pub(crate) fn segment_count(bytes: &[u8]) -> usize {
    bytes
        .split(|&b| b == b'\n')
        .filter(|line| event_type(line) == Some("end"))
        .count()
}

/// The raw JSONL text of the last settled segment's `error` event, when it
/// carries one (§4.4 *failed* framing; §5.1 #13 "response/error text"). Returns
/// `Some(line)` **iff** [`framing`] would return [`Framing::Failed`] — the same
/// last-segment traversal, stopping at the first `error` before the previous
/// segment boundary; `None` for complete / killed / empty. The verbatim event
/// line (its `kind`/`message`/`status` fields and all) is what the auth heuristic
/// ([`crate::login::auth`]) scans, so the classifier stays schema-agnostic (bz's
/// and lernie's error shapes may differ, §5.1 #10/#13).
pub(crate) fn error_text(bytes: &[u8]) -> Option<String> {
    let terminated = match bytes.iter().rposition(|&b| b == b'\n') {
        // `idx` is a position within `bytes`, so `..=idx` always yields `Some`.
        Some(idx) => bytes.get(..=idx).unwrap_or(bytes),
        None => return None,
    };
    let lines: Vec<&[u8]> = terminated
        .split(|&b| b == b'\n')
        .filter(|l| !l.is_empty())
        .collect();
    let (last, rest) = lines.split_last()?;
    if event_type(last) != Some("end") {
        return None;
    }
    for line in rest.iter().rev() {
        match event_type(line) {
            Some("end") => return None,
            Some("error") => return Some(String::from_utf8_lossy(line).into_owned()),
            _ => {}
        }
    }
    None
}

/// Is the payload a §4.4 *complete* model call? `false` for failed, killed,
/// and empty files — the live classifier's boolean view of [`framing`].
pub(super) fn last_segment_complete(bytes: &[u8]) -> bool {
    framing(bytes) == Framing::Complete
}

/// The classifier-relevant `type` field of one JSONL event line, or `None`
/// if it does not parse as a JSON object with a recognized string `type`.
fn event_type(line: &[u8]) -> Option<&'static str> {
    let value: serde_json::Value = serde_json::from_slice(line).ok()?;
    match value.get("type")?.as_str()? {
        "end" => Some("end"),
        "finish" => Some("finish"),
        "error" => Some("error"),
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const FINISH_END: &[u8] = br#"{"type":"message_start","v":1,"role":"assistant"}
{"type":"finish","reason":"stop"}
{"type":"end"}
"#;
    const ERROR_END: &[u8] = br#"{"type":"message_start","v":1,"role":"assistant"}
{"type":"error","kind":"transport","message":"reset"}
{"type":"end"}
"#;

    #[test]
    fn finish_then_end_is_complete() {
        assert!(last_segment_complete(FINISH_END));
    }

    #[test]
    fn error_then_end_is_not_complete() {
        // A failed attempt (error+end) is *failed*, not complete (§4.4).
        assert!(!last_segment_complete(ERROR_END));
    }

    #[test]
    fn no_trailing_end_is_not_complete() {
        let jsonl = br#"{"type":"content_delta","index":0,"delta":{"text_delta":"hi"}}
"#;
        assert!(!last_segment_complete(jsonl));
    }

    #[test]
    fn empty_or_newline_only_is_not_complete() {
        assert!(!last_segment_complete(b""));
        assert!(!last_segment_complete(b"\n\n"));
    }

    #[test]
    fn trailing_partial_line_after_end_is_ignored() {
        let jsonl = b"{\"type\":\"finish\",\"reason\":\"stop\"}\n{\"type\":\"end\"}\n{partial";
        assert!(last_segment_complete(jsonl));
    }

    #[test]
    fn only_latest_segment_decides_complete() {
        // A prior failed attempt then a clean retry: complete.
        let jsonl = br#"{"type":"error","kind":"x"}
{"type":"end"}
{"type":"message_start","v":1}
{"type":"finish","reason":"stop"}
{"type":"end"}
"#;
        assert!(last_segment_complete(jsonl));
    }

    #[test]
    fn latest_segment_error_after_earlier_finish_is_not_complete() {
        // A clean attempt then a failed retry: failed (last segment wins).
        let jsonl = br#"{"type":"finish","reason":"stop"}
{"type":"end"}
{"type":"error","kind":"x"}
{"type":"end"}
"#;
        assert!(!last_segment_complete(jsonl));
    }

    #[test]
    fn end_without_finish_or_error_is_not_complete() {
        // Defensive: an `end` with neither finish nor error in its segment
        // is not a clean completion.
        assert!(!last_segment_complete(
            b"{\"type\":\"message_start\"}\n{\"type\":\"end\"}\n"
        ));
    }

    #[test]
    fn malformed_last_line_is_not_complete() {
        assert!(!last_segment_complete(b"{\"type\":\"finish\"}\nnot json\n"));
    }

    #[test]
    fn framing_classifies_the_three_outcomes() {
        // Complete, Failed (error segment), Killed (no trailing end) — the
        // three states the steps inspector renders per step (§15 Y13).
        assert_eq!(framing(FINISH_END), Framing::Complete);
        assert_eq!(framing(ERROR_END), Framing::Failed);
        assert_eq!(
            framing(b"{\"type\":\"content_delta\",\"index\":0}\n"),
            Framing::Killed
        );
        // An `end` with neither finish nor error is not a clean completion.
        assert_eq!(
            framing(b"{\"type\":\"message_start\"}\n{\"type\":\"end\"}\n"),
            Framing::Killed
        );
    }

    #[test]
    fn error_text_returns_the_error_line_iff_framing_is_failed() {
        // Failed: the verbatim error event line comes back (its status/message the
        // auth heuristic reads); complete and killed yield None — error_text Some
        // ⟺ framing Failed.
        let failed = br#"{"type":"error","kind":"http","status":401}
{"type":"end"}
"#;
        assert_eq!(
            error_text(failed).as_deref(),
            Some(r#"{"type":"error","kind":"http","status":401}"#)
        );
        assert_eq!(framing(failed), Framing::Failed);
        assert_eq!(error_text(FINISH_END), None); // complete
        assert_eq!(error_text(b"{\"type\":\"content_delta\"}\n"), None); // killed
        assert_eq!(error_text(b""), None); // empty
        assert_eq!(error_text(b"\n\n"), None); // newline-only: no settled segment
        assert_eq!(error_text(b"no trailing newline"), None); // unterminated
    }

    #[test]
    fn error_text_reads_only_the_latest_segment() {
        // A failed attempt then a clean retry: complete, so no error text (the
        // prior segment's error is behind an `end` boundary the walk stops at).
        let retried = br#"{"type":"error","kind":"x"}
{"type":"end"}
{"type":"finish","reason":"stop"}
{"type":"end"}
"#;
        assert_eq!(error_text(retried), None);
        assert_eq!(framing(retried), Framing::Complete);
    }

    #[test]
    fn segment_count_counts_end_events() {
        // Two completed attempts (an errored retry then a clean one) → 2.
        let jsonl = br#"{"type":"error","kind":"x"}
{"type":"end"}
{"type":"finish","reason":"stop"}
{"type":"end"}
"#;
        assert_eq!(segment_count(jsonl), 2);
        // A single in-flight segment with no trailing `end` → 0 completed.
        assert_eq!(
            segment_count(b"{\"type\":\"content_delta\",\"index\":0}\n"),
            0
        );
        assert_eq!(segment_count(b""), 0);
    }
}