Files
Kigi-CLI/crates/common/kigi-compaction/src/code_compaction/failure.rs
T
ZacharyZhang-NY a02b555e66 docs(comments): rewrite comments across all crates to the guidelines
Sweep every first-party crate source (1956 .rs files) to the project comment
guidelines: delete redundant restatements, decorative banners, change
narration, and end-of-line comments; keep and tighten the crucial ones
(invariants, bug rationale, SAFETY blocks, ported-source attribution).

No functional code changed. Every edit is proven comment-only against the
prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a
separate doctest-fence check. Where removing a comment made rustfmt or clippy
want to re-lay-out adjacent code, the minimal triggering comment is restored so
code tokens stay byte-identical.

Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy
--workspace --all-targets (0 warnings).

Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for
these guidelines (flags banners, end-of-line comments, change narration, and
commented-out code).
2026-07-23 16:55:39 -04:00

193 lines
6.5 KiB
Rust

//! Deterministic-vs-transient failure classification for compaction
//! LLM calls.
//!
//! The *policy* lives here, shared across harnesses; per-harness error types
//! and their wrapping (e.g. kigi's `SamplingError` →
//! `CompactFailure(acp::Error)`) stay in thin host wrappers that delegate the
//! status/message decisions to these functions.
/// Whether a compaction-call failure is worth retrying.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FailureKind {
/// Retrying the same payload will hit the same failure — the retry loop
/// should bail without sleeping or re-issuing.
Deterministic,
/// Failure may resolve on retry (network blips, 5xx, rate limits).
Transient,
}
impl FailureKind {
pub fn is_deterministic(self) -> bool {
matches!(self, Self::Deterministic)
}
}
/// True when an error message indicates a context-window overflow.
///
/// Backends report this inconsistently with no stable error code, so the match
/// is on message text. Re-sending the same payload always fails, so callers
/// must not retry.
pub fn is_context_length_error(message: &str) -> bool {
let m = message.to_ascii_lowercase();
m.contains("too long for this model")
|| m.contains("prompt is too long")
|| m.contains("maximum prompt length")
|| m.contains("maximum context length")
|| m.contains("context_length_exceeded")
}
/// Classify an HTTP API failure (status + message) for the compaction retry
/// loop.
///
/// A context-length overflow message is deterministic regardless of status,
/// because backends sometimes dress it as a synthesized 500.
pub fn classify_http_status(status: u16, message: &str) -> FailureKind {
if is_context_length_error(message)
|| ((400..500).contains(&status) && status != 408 && status != 429)
{
FailureKind::Deterministic
} else {
FailureKind::Transient
}
}
/// Classify a provider-style stream error event (`ResponseError` /
/// `ResponseFailed.error`) for the compaction retry loop.
///
/// `code` is the structured `code` field on the event: typically a numeric HTTP
/// status as a string, but some providers send error-type strings like
/// `"invalid_request_error"` instead, and that marker can also arrive buried in
/// `message` alone. It always means a schema violation, which re-sending the
/// same payload cannot fix.
pub fn classify_stream_event_error(code: Option<&str>, message: &str) -> FailureKind {
if matches!(code, Some("invalid_request_error")) || message.contains("invalid_request_error") {
return FailureKind::Deterministic;
}
if let Some(status_code) = code.and_then(|c| c.parse::<u16>().ok())
&& (400..500).contains(&status_code)
&& status_code != 408
&& status_code != 429
{
return FailureKind::Deterministic;
}
// Size overflow arrives here with no parseable code (`code="none"`); the
// message is the only signal that re-sending cannot help.
if is_context_length_error(message) {
return FailureKind::Deterministic;
}
FailureKind::Transient
}
#[cfg(test)]
mod tests {
use super::*;
fn det_status(status: u16) -> bool {
classify_http_status(status, "test").is_deterministic()
}
#[test]
fn http_4xx_is_deterministic_except_408_and_429() {
assert!(det_status(400));
assert!(det_status(401));
assert!(det_status(403));
assert!(det_status(404));
assert!(det_status(413));
assert!(!det_status(408));
assert!(!det_status(429));
assert!(!det_status(500));
assert!(!det_status(502));
assert!(!det_status(503));
}
#[test]
fn http_500_with_context_length_message_is_deterministic() {
// The sampler synthesizes status=500 from a streamed size overflow, so
// status alone reads transient; the message must still short-circuit.
assert!(
classify_http_status(
500,
"API error (status 500 Internal Server Error): \
The prompt is too long for this model's context window."
)
.is_deterministic()
);
}
#[test]
fn stream_event_invalid_request_error_marker_is_deterministic() {
assert!(
classify_stream_event_error(
Some("invalid_request_error"),
"messages.27.content.1: ..."
)
.is_deterministic()
);
assert!(
classify_stream_event_error(
Some("400"),
"Provider returned invalid_request_error: messages.X..."
)
.is_deterministic()
);
assert!(
classify_stream_event_error(None, "messages.X.content.Y: invalid_request_error: ...")
.is_deterministic()
);
}
#[test]
fn stream_event_numeric_codes_match_http_classification() {
let det = |c: &str| classify_stream_event_error(Some(c), "msg").is_deterministic();
assert!(det("400"));
assert!(det("401"));
assert!(det("403"));
assert!(det("404"));
assert!(!det("408"));
assert!(!det("429"));
assert!(!det("500"));
assert!(!det("503"));
}
#[test]
fn stream_event_unknown_code_defaults_to_transient() {
assert!(!classify_stream_event_error(None, "msg").is_deterministic());
assert!(!classify_stream_event_error(Some("error"), "msg").is_deterministic());
assert!(!classify_stream_event_error(Some("overloaded_error"), "msg").is_deterministic());
}
#[test]
fn stream_event_context_length_message_is_deterministic() {
assert!(
classify_stream_event_error(
None,
"The prompt is too long for this model's context window."
)
.is_deterministic()
);
}
#[test]
fn context_length_error_matches_known_messages() {
for msg in [
"The prompt is too long for this model's context window.",
"prompt is too long: 250000 tokens > 200000 maximum",
"exceeds the maximum prompt length",
"This model's maximum context length is 128000 tokens",
"error code: context_length_exceeded",
] {
assert!(is_context_length_error(msg), "should match: {msg}");
}
for msg in [
"internal server error",
"rate limited",
"connection reset by peer",
] {
assert!(!is_context_length_error(msg), "should not match: {msg}");
}
}
}