//! Canonical partial-result streaming contract shared by every streaming tool. //! //! A tool declares a [`StreamingSpec`] in its [`ToolCapabilities`] and emits //! deltas from `execute` via [`stream_chunk`], which materializes the spec into //! a [`PartialResultPayload`] carried by [`ToolProgress::Custom`]. Downstream //! layers dispatch on the envelope's `subkind` rather than on the tool's //! identity. use kigi_tool_protocol::StreamingSpec; use serde::{Deserialize, Serialize}; use crate::tool::ToolProgress; /// Per-frame `delta` byte cap used when [`StreamingSpec::max_delta_bytes`] is /// unset. Guards against a single oversized tick flooding the harness in one /// frame. Deliberately independent of `ToolCapabilities::max_frame_bytes`, /// which caps whole frames (16 MiB ceiling), not deltas. const DEFAULT_MAX_DELTA_BYTES: usize = 16 * 1024; /// Canonical payload carried by a streaming tool's [`ToolProgress::Custom`]. /// /// Downstream layers dispatch on the envelope's `subkind`. Deltas are /// append-only and lossless (see [`stream_chunk`]). /// /// Parsed strictly (`deny_unknown_fields`): an unexpected field is a hard /// deserialize error rather than being silently ignored, so producer/consumer /// schema drift (e.g. a stale field from an un-updated producer) is caught and /// the frame is dropped with a warning instead of misinterpreted. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct PartialResultPayload { /// Content produced since the previous tick (the delta). pub delta: String, /// Monotonic total bytes produced so far (NOT the current buffer length). pub total_bytes: u64, /// Cumulative content was lost upstream and will never be delivered /// (distinct from a single-tick `gap`). #[serde(default)] pub truncated: bool, /// This delta has a gap: a single oversized tick overflowed the tail /// buffer and its middle was dropped. #[serde(default)] pub gap: bool, } /// Byte count of an incomplete (still-arriving) UTF-8 sequence at the very end /// of `bytes`, or 0 when the slice ends on a complete sequence or in invalid /// bytes that can never become valid (those are surfaced lossily instead of /// held forever). fn incomplete_utf8_suffix_len(bytes: &[u8]) -> usize { match std::str::from_utf8(bytes) { Ok(_) => 0, // `error_len() == None` means the error is an incomplete sequence at // the end of the input — the only case worth holding back. Err(e) if e.error_len().is_none() => bytes.len() - e.valid_up_to(), Err(_) => 0, } } /// Build at most one [`ToolProgress::Custom`] delta from a monotonic byte /// source, with UTF-8-safe slicing at both the tick boundary and the per-frame /// cap. /// /// `tail` is the source's (possibly truncated) tail buffer — the newest bytes /// are always at its end. `total` is the monotonic count of bytes produced so /// far; `last_total` records how much has already been surfaced and is advanced /// in place. Returns `None` when `total` has not advanced (no new bytes). /// /// Deltas are **append-only and lossless**: when a delta would end mid-way /// through a multi-byte UTF-8 sequence, or exceeds the per-frame cap /// ([`StreamingSpec::max_delta_bytes`], default 16 KiB), the excess bytes are /// *held back* — `last_total` advances only past the emitted bytes, so the next /// call re-slices the remainder from the (still-growing) tail. Concatenated /// deltas are therefore always valid UTF-8 and lossless. /// /// `truncated` is the caller's cumulative upstream-truncation flag (e.g. a /// source that hit a hard output cap and will never deliver the elided bytes). /// It is copied into the payload verbatim and is intentionally distinct from /// the per-tick `gap` (a single oversized tick overflowed the tail buffer and /// its middle was dropped upstream). Sources with no cumulative-truncation /// notion pass `false`. pub fn stream_chunk( spec: &StreamingSpec, tail: &[u8], total: u64, last_total: &mut u64, truncated: bool, ) -> Option { if total <= *last_total { return None; } let new = total - *last_total; let tail_len = tail.len() as u64; // Deltas are keyed off the monotonic `total`, not the buffer length: when // all genuinely-new bytes still fit in the tail we slice its suffix; when a // single tick's burst exceeded the buffer the middle was dropped upstream, // so we emit what survived plus a `gap` marker. let (delta_bytes, gap) = if new <= tail_len { (&tail[(tail_len - new) as usize..], false) } else { (tail, true) }; let cap = spec .max_delta_bytes .map_or(DEFAULT_MAX_DELTA_BYTES, |c| c as usize); // Defer: emit the longest prefix that fits the cap AND ends on a complete // UTF-8 sequence; hold the rest back for the next call (the tail still // contains it, since `last_total` only advances past the emitted bytes). // Nothing is dropped. let mut cut = delta_bytes.len().min(cap); while cut > 0 && incomplete_utf8_suffix_len(&delta_bytes[..cut]) > 0 { cut -= 1; } // A cap smaller than one multi-byte char would deadlock at cut == 0 while // bytes remain; emit the full first char in that pathological case rather // than stalling forever. if cut == 0 && !delta_bytes.is_empty() { cut = delta_bytes.len().min(4); while cut < delta_bytes.len() && incomplete_utf8_suffix_len(&delta_bytes[..cut]) > 0 { cut += 1; } } if cut == 0 { return None; } let delta = String::from_utf8_lossy(&delta_bytes[..cut]).into_owned(); let consumed = cut as u64; // Advance only past what was emitted (gap case: the upstream-dropped // middle counts as consumed — those bytes can never be re-sliced). *last_total = if gap { total - (delta_bytes.len() as u64 - consumed.min(delta_bytes.len() as u64)) } else { *last_total + consumed }; let payload = PartialResultPayload { delta, total_bytes: total, // The caller's cumulative upstream-loss flag passes through verbatim; // the per-tick `gap` is reported separately. truncated, gap, }; Some(ToolProgress::Custom { subkind: spec.subkind.clone(), // Infallible: a struct of String/u64/bool/Copy-enums has no map keys // or floats that could make `to_value` fail. payload: serde_json::to_value(&payload).expect("PartialResultPayload always serializes"), }) } #[cfg(test)] mod tests { use super::*; fn spec_with(max_delta_bytes: Option) -> StreamingSpec { StreamingSpec { subkind: "test_chunk".to_owned(), max_delta_bytes, } } /// Run `stream_chunk`, assert it produced a frame, and decode the payload. fn run( spec: &StreamingSpec, tail: &[u8], total: u64, last_total: &mut u64, truncated: bool, ) -> PartialResultPayload { let progress = stream_chunk(spec, tail, total, last_total, truncated) .expect("expected a progress frame"); let ToolProgress::Custom { subkind, payload } = progress else { panic!("expected ToolProgress::Custom"); }; assert_eq!(subkind, "test_chunk"); serde_json::from_value(payload).expect("payload decodes") } #[test] fn no_new_bytes_returns_none_and_leaves_last_total() { let spec = spec_with(None); let mut last = 10; assert!(stream_chunk(&spec, b"abc", 10, &mut last, false).is_none()); assert!(stream_chunk(&spec, b"abc", 5, &mut last, false).is_none()); assert_eq!( last, 10, "last_total is untouched when total does not advance" ); } #[test] fn emits_suffix_delta_and_advances_last_total() { let spec = spec_with(None); let mut last = 2; // total 2 -> 5: 3 genuinely-new bytes, all present in the tail suffix. let p = run(&spec, b"abcde", 5, &mut last, false); assert_eq!(p.delta, "cde"); assert_eq!(p.total_bytes, 5); assert!(!p.gap); assert!(!p.truncated); assert_eq!(last, 5, "last_total advanced in place"); } #[test] fn gap_set_when_new_exceeds_surviving_tail() { // 100 new bytes but only a 4-byte tail survived upstream: the middle // was dropped, so the whole tail is emitted with gap = true. let spec = spec_with(None); let mut last = 0; let p = run(&spec, b"tail", 100, &mut last, false); assert_eq!(p.delta, "tail"); assert_eq!(p.total_bytes, 100); assert!(p.gap); assert!( !p.truncated, "a per-tick gap must not set cumulative truncated" ); assert_eq!(last, 100, "fully-emitted gap delta consumes the total"); } #[test] fn truncated_is_caller_supplied_and_distinct_from_gap() { let spec = spec_with(None); let mut last = 0; // Caller reports cumulative upstream truncation; no per-tick gap here. let p = run(&spec, b"abc", 3, &mut last, true); assert!( p.truncated, "caller's cumulative flag passes through verbatim" ); assert!(!p.gap, "no tail overflow this tick"); } #[test] fn append_multibyte_split_across_ticks_is_held_back_and_reassembled() { // Tick 1 delivers "aé" cut mid-'é' (0xC3 without 0xA9). The lone lead // byte is held back, NOT emitted as U+FFFD. let spec = spec_with(None); let mut last = 0; let p = run(&spec, b"a\xC3", 2, &mut last, false); assert_eq!(p.delta, "a", "incomplete UTF-8 suffix held back"); assert_eq!(last, 1, "last_total advances only past emitted bytes"); // Tick 2: the continuation byte arrives; the held bytes re-slice from // the tail and the char comes out whole. let p = run(&spec, "aé".as_bytes(), 3, &mut last, false); assert_eq!(p.delta, "é", "held bytes reassemble into a whole char"); assert_eq!(last, 3); } #[test] fn append_over_cap_defers_remainder_to_next_call_without_loss() { // Cap 4: a 9-byte burst is paced out over capped frames; nothing is // dropped and the concatenation is lossless. let spec = spec_with(Some(4)); let mut last = 0; let mut out = String::new(); while last < 9 { let p = run(&spec, b"abcdefghi", 9, &mut last, false); assert!(p.delta.len() <= 4, "every frame respects the cap"); out.push_str(&p.delta); } assert_eq!(out, "abcdefghi", "deferred remainders are all emitted"); assert_eq!(last, 9); } #[test] fn append_cap_cut_respects_utf8_boundaries() { // 7 ASCII bytes + 'é' (2 bytes) = 9 bytes. A cap of 8 would split the // 'é'; the cut backs off and the 'é' is deferred whole. let tail = "aaaaaaaé".as_bytes(); let spec = spec_with(Some(8)); let mut last = 0; let p = run(&spec, tail, tail.len() as u64, &mut last, false); assert_eq!(p.delta, "aaaaaaa", "backed off the split multibyte char"); let p = run(&spec, tail, tail.len() as u64, &mut last, false); assert_eq!(p.delta, "é"); } #[test] fn payload_decodes_with_optional_flags_absent() { // Wire tolerance: a payload missing the bool/count fields still // decodes (serde defaults), matching the ToolCapabilities convention. let p: PartialResultPayload = serde_json::from_value(serde_json::json!({ "delta": "x", "total_bytes": 1, })) .expect("payload with absent flags decodes"); assert!(!p.truncated); assert!(!p.gap); } #[test] fn payload_rejects_unknown_field() { // Strict (`deny_unknown_fields`): a stale/typo'd field — e.g. a removed // `accumulation` from an un-updated producer — is a hard error, not // silently ignored, so schema drift never decodes into a partial frame. let decoded = serde_json::from_value::(serde_json::json!({ "delta": "x", "total_bytes": 1, "truncated": false, "gap": false, "accumulation": "append", })); assert!( decoded.is_err(), "an unknown field must be rejected under deny_unknown_fields" ); } // ── Limit / latency invariants ────────────────────────────────────────── /// A backlog drains in exactly `ceil(new / cap)` calls — no extra round-trips. #[test] fn drains_backlog_in_minimum_ticks() { let cap = 4usize; let spec = spec_with(Some(cap as u32)); let data = b"abcdefghij"; let total = data.len() as u64; let mut last = 0; let mut ticks = 0usize; let mut out = String::new(); while last < total { out.push_str(&run(&spec, data, total, &mut last, false).delta); ticks += 1; assert!(ticks <= 100, "must terminate"); } assert_eq!(out, "abcdefghij", "lossless"); assert_eq!( ticks, data.len().div_ceil(cap), "no extra ticks beyond ceil(new / cap)" ); } /// ASCII frames fill to the cap — no under-fill, no empty trailing frame. #[test] fn ascii_frames_fill_to_cap() { let spec = spec_with(Some(4)); let data = b"abcdefgh"; let mut last = 0; assert_eq!(run(&spec, data, 8, &mut last, false).delta, "abcd"); assert_eq!(run(&spec, data, 8, &mut last, false).delta, "efgh"); assert!( stream_chunk(&spec, data, 8, &mut last, false).is_none(), "no spurious empty trailing frame" ); } /// A delta exactly at the cap is one frame, no gap, nothing deferred. #[test] fn exact_cap_emits_single_frame() { let cap = 8u64; let spec = spec_with(Some(cap as u32)); let mut last = 0; let p = run(&spec, b"abcdefgh", cap, &mut last, false); assert_eq!(p.delta, "abcdefgh"); assert!(!p.gap); assert_eq!(last, cap); assert!(stream_chunk(&spec, b"abcdefgh", cap, &mut last, false).is_none()); } /// A cap smaller than one char still emits a whole char — never stalls. #[test] fn tiny_cap_below_char_still_makes_progress() { let spec = spec_with(Some(1)); let mut last = 0; let p = run(&spec, "é".as_bytes(), 2, &mut last, false); assert_eq!( p.delta, "é", "emits the whole first char despite cap < charlen" ); assert_eq!(last, 2, "and makes forward progress"); } /// UTF-8 backoff loses at most 3 bytes, so frames stay within 3 of the cap. #[test] fn utf8_backoff_stays_within_three_bytes_of_cap() { let cap = 7usize; // splits a 4-byte char -> backs off to 4 (cap - 3) let spec = spec_with(Some(cap as u32)); let data = "😀😀😀😀".as_bytes(); let total = data.len() as u64; let mut last = 0; while last < total { let n = run(&spec, data, total, &mut last, false).delta.len(); assert!( n.is_multiple_of(4) && n >= 4, "emits whole 4-byte chars, got {n}" ); assert!( n >= cap - 3, "frame stays within 3 bytes of the cap, got {n}" ); assert!(n <= cap, "frame respects the cap, got {n}"); } assert_eq!(last, total, "drains losslessly"); } /// A gap drains only the surviving tail; never re-scans the dropped middle. #[test] fn gap_with_cap_paces_surviving_tail_and_terminates() { let spec = spec_with(Some(4)); let tail = b"abcdefgh"; let total = 1000u64; // only 8 of 1000 bytes survived in the tail let mut last = 0; let mut ticks = 0usize; let mut emitted = 0usize; let mut saw_gap = false; while last < total { let Some(progress) = stream_chunk(&spec, tail, total, &mut last, false) else { break; }; let ToolProgress::Custom { payload, .. } = progress else { panic!("expected ToolProgress::Custom"); }; let p: PartialResultPayload = serde_json::from_value(payload).unwrap(); saw_gap |= p.gap; emitted += p.delta.len(); ticks += 1; assert!( ticks <= 4, "gap pacing must drain only the surviving tail, not re-scan the dropped middle" ); } assert!(saw_gap, "first frame reports the gap"); assert_eq!( emitted, tail.len(), "emits exactly the surviving tail bytes — no replay of dropped middle" ); } }