Files
Kigi-CLI/crates/common/kigi-tool-runtime/src/streaming.rs
T
ZacharyZhang-NY d6c20fc13f M0: compilable skeleton — Kigi 0.1.0 fork surgery
Hard fork of xai-org/grok-build (Apache-2.0) re-targeted as Kigi, an
unofficial Kimi Code CLI community build.

Rename & identity
- 72 xai-*/xai-grok-* crates -> kigi-* (explicit: xai-grok-pager-bin ->
  kigi-bin [binary `kigi`], xai-grok-pager -> kigi-tui; rest mechanical);
  ptyctl, ptyctl-cli, third_party/ unchanged; proto package
  xai.grok.tools.v1 -> kigi.tools.v1
- Config home ~/.kigi (KIGI_SHARE_DIR override), env prefix GROK_* ->
  KIGI_*, `kigi --version` carries the unofficial-community-build notice
- clap identity, help text, startup banner, prompt templates rebranded
  (templates re-encrypted)

Deletions (PRD removal list #5/#6/#7/#9/#10)
- voice input (xai-grok-voice) and all TUI wiring
- telemetry: Mixpanel client, external OTel stream, Sentry, OTLP layers,
  trace/GCS/S3 upload queues (kigi-file-utils halved), workspace upload
  module & dc_log, heap-profile uploader, auth-diagnostics uploader,
  session-analytics halves of feedback; local zero-egress observability
  preserved in new kigi-log crate (unified log, --debug firehose,
  subsystem file logs, opt-in instrumentation)
- announcements (crate, remote-settings fields, TUI surfaces)
- plugin marketplace (crate, sources/browse/CTA/extensions-modal tab);
  direct plugin install/uninstall/update via kigi-agent git_install kept
- relay/gateway/assets endpoints and features (agent relay, headless
  relay transport, gateway bridge, LeaderEnvUrls); leader IPC socket now
  ~/.kigi/leader.sock + KIGI_LEADER_SOCKET, no ws-url derivation
- functional types rehomed instead of deleted: PermissionMode ->
  kigi-config-types, McpInitStrategy -> kigi-mcp, PrCreationSource ->
  session signals, TerminalDiagnostics -> kigi-pager-render, agent_id ->
  shell util

Endpoints
- kigi-env rewritten: single production KigiEndpoints {coding_api_base_url
  https://api.kimi.com/coding/v1 (KIGI_CODE_BASE_URL), oauth_host
  https://auth.kimi.com (KIGI_OAUTH_HOST), update_base_url (GitHub
  Releases API), upgrade_page_url}; GrokBuildEnvironment enum deleted

Toolchain & workspace hygiene
- Rust 1.97.0 pinned; edition 2024; full cargo update; git2 hoisted to
  workspace at 0.21 (Option->Result API migration), quick-xml 0.41
- Root Cargo.toml hand-maintained (PRD §8.1): version 0.1.0 inherited by
  all members, members sorted, unused deps pruned
- cargo-deny advisories gate (deny.toml with documented transitive
  exceptions); CI workflow (check/clippy/fmt/deny/test, macOS+Linux)
- cross-crate test seams re-gated behind `test-support` cargo feature;
  insta snapshot baselines renamed to the kigi_tui prefix
- clippy --workspace --all-targets: zero warnings; fmt clean

Fixes surfaced by the port
- updater probe/installer divergence (bin/kigi vs bin/grok symlink set)
- idle model-metadata refresh dead under KIGI_CODE_BASE_URL override
  (new is_effective_coding_endpoint_url, loopback+override aware)
- macOS symlinked-TMPDIR fixture canonicalization (foreign_sessions,
  fast-worktree); RSS measurement tests serialized via serial_test

Docs & legal (Apache §4)
- NOTICE added (upstream attribution + change statement); THIRD-PARTY
  notices sustained; kigi-tools ported-code notices extended; README,
  CONTRIBUTING, SECURITY, AGENTS.md rewritten

Out of scope for M0 (tracked): Kimi auth/inference (M1), search/fetch,
command parity, config import (M2), Computer Hub excision & final
brand-token sweep (M2), distribution & self-update rewrite (M3).
2026-07-17 05:31:01 -04:00

440 lines
17 KiB
Rust

//! 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<ToolProgress> {
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<u32>) -> 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::<PartialResultPayload>(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"
);
}
}