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).
This commit is contained in:
@@ -0,0 +1,366 @@
|
||||
//! Configuration for intra-compaction.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Which targets intra-compaction may compact.
|
||||
///
|
||||
/// - `FullReplace` (default): grok-build's full-replace strategy — summarize
|
||||
/// the *whole* conversation (prior history + accumulated steps) and rebuild
|
||||
/// context from scratch as `[system] + [summary]`. Drives the shared
|
||||
/// `code_compaction` summarizer directly; no tail is kept.
|
||||
/// - `StepsOnly`: only compact accumulated step turns within the current
|
||||
/// agent loop (keeps the recent tail).
|
||||
/// - `HistoryOnly`: only compact prior conversation history; leave the
|
||||
/// current loop's accumulated step turns alone.
|
||||
/// - `HistoryThenSteps`: compact history first, then — only if the
|
||||
/// accumulated step turns still account for a large enough share of the
|
||||
/// prompt (controlled by [`IntraCompactionConfig::steps_trigger_ratio`]) —
|
||||
/// also compact the current loop's steps.
|
||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum IntraCompactionMode {
|
||||
#[default]
|
||||
FullReplace,
|
||||
StepsOnly,
|
||||
HistoryOnly,
|
||||
HistoryThenSteps,
|
||||
}
|
||||
|
||||
/// Which *summarization algorithm* intra-compaction uses to turn the selected
|
||||
/// turns into the replacement summary. Orthogonal to [`IntraCompactionMode`]
|
||||
/// (which picks *what* to compact); this picks *how* the summary is produced.
|
||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum IntraSummarizer {
|
||||
/// New (default): the shared summarization core — `build_summary_prompt`
|
||||
/// + degenerate-reject + `format_compact_summary` cleaning.
|
||||
#[default]
|
||||
Shared,
|
||||
/// Previous intra algorithm: per-target prompt (`format_compaction_prompt`
|
||||
/// / history dev+user prompts), no cleaning. Kept for switchability.
|
||||
Legacy,
|
||||
}
|
||||
|
||||
/// Intra-compaction configuration for an agent's sample loop.
|
||||
///
|
||||
/// This is the intra-compaction analog of
|
||||
/// [`InterCompactionConfig`](crate::inter_compaction::InterCompactionConfig).
|
||||
/// The structural difference is *where the config lives*:
|
||||
/// - inter-compaction runs as a singleton between-turn service, so it has one
|
||||
/// global config resolved from service YAML.
|
||||
/// - intra-compaction runs **per-agent** inside the harness sampler loop, so
|
||||
/// this struct is embedded directly in each agent's spec. Defaults come from
|
||||
/// the [`Default`] impl below; an agent can optionally override them under
|
||||
/// `agents.<name>.intra_compaction` in agent config YAML (none set today).
|
||||
/// There is no standalone service config for it.
|
||||
///
|
||||
/// When `enabled = false` (default), no intra-compaction runs for that agent.
|
||||
///
|
||||
/// Uses **percentage** thresholds (borrowed from grok-shell's
|
||||
/// `CompactionPolicy`) for portability across models with different context
|
||||
/// windows.
|
||||
///
|
||||
/// The fields are split into two groups: a **common** block that every mode
|
||||
/// stores (enablement, trigger gate fields, reduction guards, the compaction
|
||||
/// LLM call, audit) and a **mode-specific** block whose fields are each read by
|
||||
/// only a subset of modes (see the per-field `[...]` tags). In particular,
|
||||
/// `FullReplace` — the default — ignores `min_steps_before_compact` at trigger
|
||||
/// time (token threshold only, matching grok-build) and also ignores
|
||||
/// `summarizer`, `target_threshold_percent`, `steps_trigger_ratio`, and
|
||||
/// `user_message_truncate_chars`. The field remains on this config for all
|
||||
/// modes (YAML / remote agent config / defaults); only enforcement is mode-dependent.
|
||||
///
|
||||
/// **Unset / blank → default.** Every field has a default value (the [`Default`]
|
||||
/// impl below). Leaving a field unset — absent in YAML, or blank in an agent
|
||||
/// config editor — keeps that default; each field's doc states its default
|
||||
/// inline as `Default: …`. Note that remote agent-config protos may only
|
||||
/// surface a *subset* of these fields (`enabled`, `mode`,
|
||||
/// `trigger_threshold_percent`, `target_threshold_percent`,
|
||||
/// `min_steps_before_compact` [ignored by FullReplace], `steps_trigger_ratio`
|
||||
/// [HistoryThenSteps], `compaction_model_name`); the remaining fields are
|
||||
/// never sent remotely and therefore always take the defaults here.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct IntraCompactionConfig {
|
||||
// ───────────────────────────── Common (all modes) ─────────────────────────────
|
||||
// Present on every config path regardless of `mode`. Some trigger fields
|
||||
// are ignored under FullReplace (see per-field docs).
|
||||
|
||||
// -- Enablement & strategy selection --
|
||||
/// Enable intra-compaction between steps. Default: `false` (disabled).
|
||||
pub enabled: bool,
|
||||
|
||||
/// Which targets intra-compaction may compact. See [`IntraCompactionMode`].
|
||||
/// Default: `FullReplace`.
|
||||
pub mode: IntraCompactionMode,
|
||||
|
||||
// -- Trigger gating: when a compaction pass fires (see `should_compact`) --
|
||||
/// Context window usage percentage (0-100) that triggers compaction.
|
||||
/// Compared against: `last_prompt_tokens / context_length.max_len`.
|
||||
/// Default: `85`.
|
||||
pub trigger_threshold_percent: u8,
|
||||
|
||||
/// Minimum number of completed steps before compaction can trigger.
|
||||
/// Default: `3`. Always stored on [`IntraCompactionConfig`]; agent YAML
|
||||
/// may set it for any mode.
|
||||
///
|
||||
/// **Enforcement:** applied for `StepsOnly` / `HistoryOnly` /
|
||||
/// `HistoryThenSteps`. **Ignored** when [`mode`](Self::mode) is
|
||||
/// [`IntraCompactionMode::FullReplace`] (token threshold alone, same idea
|
||||
/// as grok-build full-replace auto-compact). Worthless early passes are
|
||||
/// still limited by [`min_compactable_tokens`](Self::min_compactable_tokens)
|
||||
/// / reduction guards after a trigger.
|
||||
pub min_steps_before_compact: u32,
|
||||
|
||||
// -- Reduction guards: whether a produced summary is worth keeping --
|
||||
/// Minimum tokens that must be reducible before compaction is worth
|
||||
/// running. Below this, the LLM overhead outweighs the savings.
|
||||
/// Default: `5000`.
|
||||
pub min_compactable_tokens: u32,
|
||||
|
||||
/// Discard the compaction if it didn't shrink tokens below this ratio.
|
||||
/// Matches inter-compaction's `0.8` (= 20% minimum reduction) guard.
|
||||
/// Default: `0.8`.
|
||||
pub max_reduction_ratio: f64,
|
||||
|
||||
// -- Compaction LLM call (sampling) --
|
||||
/// Compaction model name. Blank/`None` → [`DEFAULT_COMPACTION_MODEL_NAME`].
|
||||
/// Prefer [`Self::effective_compaction_model_name`].
|
||||
pub compaction_model_name: Option<String>,
|
||||
|
||||
/// End-to-end timeout for the compaction LLM call.
|
||||
/// `120` by default, aligned with the inter-compaction service default.
|
||||
/// Default: `120`.
|
||||
pub sampling_timeout_secs: u64,
|
||||
|
||||
/// Max attempts for the compaction LLM call (effective value is `max(1)`).
|
||||
/// This is the *total* number of tries, not retries-on-top: `2` (default)
|
||||
/// = first try + one retry on a transient failure (timeout / empty / stream
|
||||
/// / start), with `retry_delay_secs` between tries. Matches the
|
||||
/// inter-compaction service default. Default: `2`.
|
||||
pub max_attempts: u32,
|
||||
/// Delay between retries. Default: `3`.
|
||||
pub retry_delay_secs: u64,
|
||||
|
||||
// -- Audit --
|
||||
/// Version string for the compaction (e.g. `"intra-v1"`).
|
||||
/// Recorded in audit logs. Default: `"intra-v1"`.
|
||||
pub compaction_version: String,
|
||||
|
||||
// ───────────────────────────── Mode-specific ─────────────────────────────
|
||||
// Each field below is read by only a subset of modes; the other modes
|
||||
// ignore it entirely. The bracketed `[...]` tag on each doc names the modes
|
||||
// that consume it.
|
||||
|
||||
// -- Partial modes only: StepsOnly / HistoryOnly / HistoryThenSteps.
|
||||
// FullReplace ignores both `summarizer` and `target_threshold_percent` —
|
||||
// it always uses the shared summarizer and replaces the whole
|
||||
// conversation, so it keeps no tail and never reads a target threshold. --
|
||||
/// [StepsOnly / HistoryOnly / HistoryThenSteps] Which summarization
|
||||
/// algorithm to use. See [`IntraSummarizer`]. Default: [`IntraSummarizer::Shared`].
|
||||
///
|
||||
/// Ignored by `FullReplace`, which *is* the shared `code_compaction` path
|
||||
/// and always summarizes via `Shared` regardless of this value. (Not
|
||||
/// always exposed by remote agent-config protos — defaults apply there.)
|
||||
pub summarizer: IntraSummarizer,
|
||||
|
||||
/// [StepsOnly / HistoryOnly / HistoryThenSteps] Target usage percentage
|
||||
/// after compaction. The compactor keeps enough recent turns to bring usage
|
||||
/// below this. Default: `50`.
|
||||
///
|
||||
/// Only used by the partial modes for tail-keep selection; `FullReplace`
|
||||
/// replaces everything and never reads it.
|
||||
pub target_threshold_percent: u8,
|
||||
|
||||
// -- HistoryThenSteps only --
|
||||
/// [HistoryThenSteps mode] Only compact accumulated step turns when their
|
||||
/// token count exceeds this fraction of the history token count.
|
||||
///
|
||||
/// Rationale: when both history and current steps are large, compacting
|
||||
/// history first usually buys enough budget. Compacting recent steps
|
||||
/// loses fine-grained context (tool results, code snippets, recent
|
||||
/// errors) and should only happen when steps themselves are large
|
||||
/// relative to history.
|
||||
///
|
||||
/// At `0.0`, steps are always compacted (after history). At very large
|
||||
/// values, steps compaction is effectively disabled in
|
||||
/// `HistoryThenSteps` mode. Default: `0.3`.
|
||||
pub steps_trigger_ratio: f64,
|
||||
|
||||
// -- History target only: HistoryOnly + HistoryThenSteps' history pass.
|
||||
// Ignored by FullReplace and StepsOnly (neither emits a user-queries
|
||||
// preamble). --
|
||||
/// [HistoryOnly / HistoryThenSteps] Character threshold above which an
|
||||
/// original user message gets middle-truncated when included in the
|
||||
/// `<grok_user_queries>` preamble prepended to the history compaction
|
||||
/// summary. Mirrors the inter-compaction Basic threshold. Has no
|
||||
/// effect for `Steps` target — steps compaction has no user-queries
|
||||
/// preamble. Default: `3000`. (Not always exposed by remote agent-config
|
||||
/// protos — defaults apply there.)
|
||||
pub user_message_truncate_chars: u32,
|
||||
}
|
||||
|
||||
/// Code-level default compaction model name (last resort).
|
||||
///
|
||||
/// Override order: agent field (non-blank) → service YAML (inter) /
|
||||
/// agent config → this constant. See crate-level docs on
|
||||
/// [`crate::DEFAULT_COMPACTION_MODEL_NAME`].
|
||||
pub const DEFAULT_COMPACTION_MODEL_NAME: &str = "grok-4.20";
|
||||
|
||||
impl IntraCompactionConfig {
|
||||
/// Agent field; blank/`None` → [`DEFAULT_COMPACTION_MODEL_NAME`].
|
||||
pub fn effective_compaction_model_name(&self) -> &str {
|
||||
self.compaction_model_name
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or(DEFAULT_COMPACTION_MODEL_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for IntraCompactionConfig {
|
||||
fn default() -> Self {
|
||||
// These are the unset/blank defaults: the value each field takes when it
|
||||
// is absent in YAML or left blank in an agent config editor.
|
||||
Self {
|
||||
// Common (all modes; min_steps stored always, enforced except FullReplace)
|
||||
enabled: false,
|
||||
mode: IntraCompactionMode::default(),
|
||||
trigger_threshold_percent: 85,
|
||||
min_steps_before_compact: 3,
|
||||
min_compactable_tokens: 5_000,
|
||||
max_reduction_ratio: 0.8,
|
||||
compaction_model_name: Some(DEFAULT_COMPACTION_MODEL_NAME.to_string()),
|
||||
sampling_timeout_secs: 120,
|
||||
max_attempts: 2,
|
||||
retry_delay_secs: 3,
|
||||
compaction_version: "intra-v1".to_string(),
|
||||
// Mode-specific
|
||||
summarizer: IntraSummarizer::default(),
|
||||
target_threshold_percent: 50,
|
||||
steps_trigger_ratio: 0.3,
|
||||
user_message_truncate_chars: 3_000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_is_disabled() {
|
||||
let p = IntraCompactionConfig::default();
|
||||
assert!(!p.enabled);
|
||||
assert_eq!(p.mode, IntraCompactionMode::FullReplace);
|
||||
assert_eq!(p.summarizer, IntraSummarizer::Shared);
|
||||
assert_eq!(p.trigger_threshold_percent, 85);
|
||||
assert_eq!(p.target_threshold_percent, 50);
|
||||
assert_eq!(
|
||||
p.compaction_model_name.as_deref(),
|
||||
Some(DEFAULT_COMPACTION_MODEL_NAME)
|
||||
);
|
||||
assert_eq!(
|
||||
p.effective_compaction_model_name(),
|
||||
DEFAULT_COMPACTION_MODEL_NAME
|
||||
);
|
||||
assert_eq!(p.max_attempts, 2);
|
||||
assert_eq!(p.retry_delay_secs, 3);
|
||||
assert!((p.steps_trigger_ratio - 0.3).abs() < f64::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blank_or_none_compaction_model_name_uses_default() {
|
||||
let none = IntraCompactionConfig {
|
||||
compaction_model_name: None,
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
none.effective_compaction_model_name(),
|
||||
DEFAULT_COMPACTION_MODEL_NAME
|
||||
);
|
||||
let empty = IntraCompactionConfig {
|
||||
compaction_model_name: Some(String::new()),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
empty.effective_compaction_model_name(),
|
||||
DEFAULT_COMPACTION_MODEL_NAME
|
||||
);
|
||||
let ws = IntraCompactionConfig {
|
||||
compaction_model_name: Some(" ".into()),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
ws.effective_compaction_model_name(),
|
||||
DEFAULT_COMPACTION_MODEL_NAME
|
||||
);
|
||||
let custom = IntraCompactionConfig {
|
||||
compaction_model_name: Some("custom-model".into()),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(custom.effective_compaction_model_name(), "custom-model");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_mode_is_full_replace() {
|
||||
assert_eq!(
|
||||
IntraCompactionMode::default(),
|
||||
IntraCompactionMode::FullReplace
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode_serde_round_trip() {
|
||||
for (mode, s) in [
|
||||
(IntraCompactionMode::FullReplace, "\"full_replace\""),
|
||||
(IntraCompactionMode::StepsOnly, "\"steps_only\""),
|
||||
(IntraCompactionMode::HistoryOnly, "\"history_only\""),
|
||||
(
|
||||
IntraCompactionMode::HistoryThenSteps,
|
||||
"\"history_then_steps\"",
|
||||
),
|
||||
] {
|
||||
let json = serde_json::to_string(&mode).unwrap();
|
||||
assert_eq!(json, s);
|
||||
let back: IntraCompactionMode = serde_json::from_str(s).unwrap();
|
||||
assert_eq!(back, mode);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summarizer_defaults_to_shared() {
|
||||
assert_eq!(IntraSummarizer::default(), IntraSummarizer::Shared);
|
||||
assert_eq!(
|
||||
IntraCompactionConfig::default().summarizer,
|
||||
IntraSummarizer::Shared
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summarizer_serde_round_trip() {
|
||||
for (s, json) in [
|
||||
(IntraSummarizer::Shared, "\"shared\""),
|
||||
(IntraSummarizer::Legacy, "\"legacy\""),
|
||||
] {
|
||||
assert_eq!(serde_json::to_string(&s).unwrap(), json);
|
||||
let back: IntraSummarizer = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(back, s);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_round_trip_with_serde_default() {
|
||||
// Partial JSON — `#[serde(default)]` fills missing fields.
|
||||
let json = r#"{
|
||||
"enabled": true,
|
||||
"trigger_threshold_percent": 80
|
||||
}"#;
|
||||
let p: IntraCompactionConfig = serde_json::from_str(json).unwrap();
|
||||
assert!(p.enabled);
|
||||
assert_eq!(p.trigger_threshold_percent, 80);
|
||||
// Defaults preserved.
|
||||
assert_eq!(p.target_threshold_percent, 50);
|
||||
assert_eq!(p.compaction_version, "intra-v1");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user