fix(compaction): configurable summarizer tool_choice; wider overflow detection

Port of upstream grok-build 0.2.105 + 0.2.118.

- CompactionToolChoice (auto default | none), resolved env
  KIGI_COMPACTION_TOOL_CHOICE > [features].compaction_tool_choice >
  remote settings. Some OpenAI-compatible servers reject
  tool_choice:"none" with tools attached, failing every long-session
  compaction; auto keeps them working (the summarization prompt already
  forbids tool use). Threaded from agent + subagent spawn through
  CompactionConfig into generate_session_compact; the Responses gate
  counts hosted tools too, so a hosted-only request still carries the
  configured choice.
- is_context_length_error recognizes the "current message ... exceeds
  budget" phrasing, so that overflow is Fatal instead of burning the
  retry budget on a deterministic failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSy9S31pZNWXtRZNVJ1sDf
This commit is contained in:
ZacharyZhang-NYandClaude Fable 5 committed 2026-08-27 10:22:18 -04:00
1 parent 6a4bff0d45
commit 1ae803519e
18 files changed
+190 -13

No files matched your search

@@ -1,6 +1,42 @@
/// Default auto-compact threshold (% of context window) when no source sets it.
pub const DEFAULT_AUTO_COMPACT_THRESHOLD_PERCENT: u8 = 85;
/// `tool_choice` sent on compaction summarizer requests when tools are
/// attached (prefix-cache alignment keeps the tool definitions in the
/// request). Default `Auto` because some OpenAI-compatible servers reject
/// `tool_choice: "none"`; the summarization prompt itself forbids tool use.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum CompactionToolChoice {
#[default]
Auto,
None,
}
impl std::str::FromStr for CompactionToolChoice {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_str() {
"auto" => Ok(Self::Auto),
"none" => Ok(Self::None),
_ => Err(()),
}
}
}
pub(crate) const ENV_COMPACTION_TOOL_CHOICE: &str = "KIGI_COMPACTION_TOOL_CHOICE";
pub(crate) fn resolve_compaction_tool_choice_from(
env: Option<&str>,
config: Option<&str>,
remote: Option<&str>,
) -> CompactionToolChoice {
env.and_then(|s| s.parse().ok())
.or_else(|| config.and_then(|s| s.parse().ok()))
.or_else(|| remote.and_then(|s| s.parse().ok()))
.unwrap_or_default()
}
/// Env-var override for `auto_compact_threshold_percent`. Parsed as `u8`;
/// out-of-range or unparseable values are ignored.
pub(crate) const ENV_AUTO_COMPACT_THRESHOLD_PERCENT: &str = "KIGI_AUTO_COMPACT_THRESHOLD_PERCENT";
@@ -127,6 +163,34 @@ pub fn resolve_compaction_wall_clock_budget_secs(gb_global: Option<u64>) -> u64
resolved
}
#[cfg(test)]
mod compaction_tool_choice_tests {
use super::{CompactionToolChoice, resolve_compaction_tool_choice_from as resolve};
#[test]
fn precedence_env_config_remote_default() {
assert_eq!(resolve(None, None, None), CompactionToolChoice::Auto);
assert_eq!(
resolve(None, None, Some("none")),
CompactionToolChoice::None
);
assert_eq!(
resolve(None, Some("auto"), Some("none")),
CompactionToolChoice::Auto
);
assert_eq!(
resolve(Some("none"), Some("auto"), Some("auto")),
CompactionToolChoice::None
);
// Unparseable tiers fall through instead of masking lower tiers.
assert_eq!(
resolve(Some("bogus"), None, Some("none")),
CompactionToolChoice::None
);
assert_eq!(resolve(Some("NONE "), None, None), CompactionToolChoice::None);
}
}
#[cfg(test)]
mod compaction_wall_clock_budget_tests {
use super::resolve_compaction_wall_clock_budget_secs as resolve;