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,399 @@
|
||||
//! grok-build's L5 wiring onto the shared full-replace engine
|
||||
//! (`kigi_compaction::code_compaction`).
|
||||
//!
|
||||
//! The shared engine drives the sample → retry → degenerate/failure
|
||||
//! classification loop via [`sample_full_replace_summary`](kigi_compaction::sample_full_replace_summary);
|
||||
//! this module adapts grok-build's transport and telemetry to its two seams:
|
||||
//!
|
||||
//! - [`ShellCompactionSampler`] wraps
|
||||
//! [`generate_session_compact`](crate::session::helpers::session_compact::generate_session_compact)
|
||||
//! as the shared [`CompactionSampler`]. It also stashes the full
|
||||
//! [`CompactOutput`] of the last successful call so the L5 loop can still
|
||||
//! record the streaming telemetry (TTFT / stream span / stop reason) that
|
||||
//! the shared [`LlmCompactionOutput`] doesn't model.
|
||||
//! - [`ShellFullReplaceObserver`] collects the per-attempt
|
||||
//! [`CompactionAttempt`] rows, rejection counters, and emits the
|
||||
//! `CompactionRetryDegraded` event — preserving the pre-migration telemetry.
|
||||
//!
|
||||
//! The verbatim → fitted → lossy **input ladder** and auto-compaction
|
||||
//! suppression stay in L5 (`compaction.rs`), driven by the
|
||||
//! `context_overflow` / `deterministic` flags on
|
||||
//! [`FullReplaceError`](kigi_compaction::FullReplaceError).
|
||||
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
|
||||
use agent_client_protocol as acp;
|
||||
use async_trait::async_trait;
|
||||
use kigi_compaction::{
|
||||
CompactionPrompt, CompactionSampleError, CompactionSampler, FullReplaceAttemptOutcome,
|
||||
FullReplaceObserver, LlmCompactionOutput,
|
||||
};
|
||||
use kigi_sampler::SamplerConfig as SamplingConfig;
|
||||
use kigi_sampling_types::{ConversationItem, HostedTool, ToolSpec};
|
||||
|
||||
use kigi_chat_state::compaction_utils::{
|
||||
CompactionAttempt, MAX_CAPTURED_SUMMARY_CHARS, bound_captured_output,
|
||||
};
|
||||
|
||||
use crate::sampling::Client as OaiCompatClient;
|
||||
use crate::session::helpers::session_compact::{
|
||||
CompactFailure, CompactOutput, build_compaction_chat_history, generate_session_compact,
|
||||
};
|
||||
|
||||
/// Wraps `generate_session_compact` as the shared engine's
|
||||
/// [`CompactionSampler`] for grok-build's full-replace pass.
|
||||
///
|
||||
/// Holds the per-call request context the seam does not carry (tools, client,
|
||||
/// session, config) and stashes the last successful [`CompactOutput`] so the
|
||||
/// caller can recover the streaming telemetry not modeled by
|
||||
/// [`LlmCompactionOutput`].
|
||||
///
|
||||
/// The summarization prompt is selected here by `use_short_prompt` (the
|
||||
/// short-prompt harness uses the short self-summarization prompt; everyone
|
||||
/// else the structured grok-build prompt), so the shared `CompactionPrompt`
|
||||
/// the engine passes is ignored — the engine builds the grok-build prompt,
|
||||
/// which equals what `build_compaction_chat_history(.., false)` appends, and
|
||||
/// the short-prompt harness needs its own variant the engine can't produce.
|
||||
pub(crate) struct ShellCompactionSampler {
|
||||
use_short_prompt: bool,
|
||||
user_context: Option<String>,
|
||||
tools: Vec<ToolSpec>,
|
||||
hosted_tools: Vec<HostedTool>,
|
||||
client: OaiCompatClient,
|
||||
session_id: acp::SessionId,
|
||||
sampling_config: SamplingConfig,
|
||||
/// Per-chunk idle timeout forwarded to `generate_session_compact`: a stalled
|
||||
/// summarizer stream (no model-output chunk for this long) fails instead of
|
||||
/// hanging.
|
||||
idle_timeout: Duration,
|
||||
/// Wall-clock budget (secs) forwarded to `generate_session_compact` as the
|
||||
/// reasoning-runaway backstop; `0` disables it.
|
||||
wall_clock_budget_secs: u64,
|
||||
/// Full output of the most recent successful sample (for L5 telemetry).
|
||||
last_success: Mutex<Option<CompactOutput>>,
|
||||
}
|
||||
|
||||
impl ShellCompactionSampler {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn new(
|
||||
use_short_prompt: bool,
|
||||
user_context: Option<String>,
|
||||
tools: Vec<ToolSpec>,
|
||||
hosted_tools: Vec<HostedTool>,
|
||||
client: OaiCompatClient,
|
||||
session_id: acp::SessionId,
|
||||
sampling_config: SamplingConfig,
|
||||
idle_timeout: Duration,
|
||||
wall_clock_budget_secs: u64,
|
||||
) -> Self {
|
||||
Self {
|
||||
use_short_prompt,
|
||||
user_context,
|
||||
tools,
|
||||
hosted_tools,
|
||||
client,
|
||||
session_id,
|
||||
sampling_config,
|
||||
idle_timeout,
|
||||
wall_clock_budget_secs,
|
||||
last_success: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Take the [`CompactOutput`] of the most recent successful sample, if any.
|
||||
pub(crate) fn take_last_success(&self) -> Option<CompactOutput> {
|
||||
self.last_success.lock().unwrap().take()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CompactionSampler for ShellCompactionSampler {
|
||||
type Item = ConversationItem;
|
||||
|
||||
async fn sample_compaction(
|
||||
&self,
|
||||
turns: &[ConversationItem],
|
||||
_prompt: &CompactionPrompt,
|
||||
_timeout: Duration,
|
||||
) -> Result<LlmCompactionOutput, CompactionSampleError> {
|
||||
// Append the harness-selected summarization prompt as the final user
|
||||
// message (compat short vs structured grok-build), ignoring the shared
|
||||
// engine's `_prompt` (see the struct doc).
|
||||
let chat_history = build_compaction_chat_history(
|
||||
turns.to_vec(),
|
||||
self.user_context.as_deref(),
|
||||
self.use_short_prompt,
|
||||
);
|
||||
|
||||
match generate_session_compact(
|
||||
chat_history,
|
||||
self.tools.clone(),
|
||||
self.hosted_tools.clone(),
|
||||
self.client.clone(),
|
||||
self.session_id.clone(),
|
||||
&self.sampling_config,
|
||||
self.idle_timeout,
|
||||
self.wall_clock_budget_secs,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(output) => {
|
||||
let response = output.content.clone();
|
||||
*self.last_success.lock().unwrap() = Some(output);
|
||||
Ok(LlmCompactionOutput {
|
||||
response,
|
||||
thinking: String::new(),
|
||||
})
|
||||
}
|
||||
Err(failure) => Err(compact_failure_to_sample_error(failure)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Map grok-build's [`CompactFailure`] onto the shared engine's
|
||||
/// [`CompactionSampleError`] so the shared retry loop classifies it the same
|
||||
/// way the in-shell loop did:
|
||||
///
|
||||
/// - `Deterministic` → [`CompactionSampleError::Build`] (whose
|
||||
/// `is_deterministic()` is `true`); a context-length overflow keeps its
|
||||
/// message text so the engine's `is_context_length_error` check fires and
|
||||
/// sets `context_overflow`.
|
||||
/// - `Transient` → [`CompactionSampleError::Other`] (`is_deterministic()` is
|
||||
/// `false`), so the engine retries it.
|
||||
fn compact_failure_to_sample_error(failure: CompactFailure) -> CompactionSampleError {
|
||||
let (deterministic, err) = match failure {
|
||||
CompactFailure::Deterministic(err) => (true, err),
|
||||
CompactFailure::Transient(err) => (false, err),
|
||||
};
|
||||
let message = acp_error_message(&err);
|
||||
if deterministic {
|
||||
CompactionSampleError::Build(message)
|
||||
} else {
|
||||
CompactionSampleError::Other(anyhow::anyhow!(message))
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the human-readable detail an `acp::Error` carries in its `data`
|
||||
/// field (where `classify_*` stash `"compact failed: <upstream>"`).
|
||||
fn acp_error_message(err: &acp::Error) -> String {
|
||||
err.data
|
||||
.as_ref()
|
||||
.and_then(|d| d.as_str())
|
||||
.unwrap_or("<no data>")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Collected telemetry from a full-replace pass, drained by the L5 loop after
|
||||
/// the shared engine returns.
|
||||
pub(crate) struct FullReplaceTelemetry {
|
||||
pub attempts: u32,
|
||||
pub attempt_details: Vec<CompactionAttempt>,
|
||||
pub degenerate_rejections: u32,
|
||||
pub transient_rejections: u32,
|
||||
pub deterministic_rejections: u32,
|
||||
/// Raw text of the last degenerate (rejected) summary, for the artifact.
|
||||
pub last_rejected_summary: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ObserverState {
|
||||
attempts: u32,
|
||||
attempt_details: Vec<CompactionAttempt>,
|
||||
degenerate_rejections: u32,
|
||||
transient_rejections: u32,
|
||||
deterministic_rejections: u32,
|
||||
last_rejected_summary: Option<String>,
|
||||
last_error_msg: Option<String>,
|
||||
}
|
||||
|
||||
/// [`FullReplaceObserver`] that reproduces grok-build's per-attempt telemetry:
|
||||
/// `CompactionAttempt` rows, rejection counters, the `CompactionRetryDegraded`
|
||||
/// event, and the warn/error tracing — without the shared engine depending on
|
||||
/// a telemetry backend.
|
||||
pub(crate) struct ShellFullReplaceObserver {
|
||||
session_id: String,
|
||||
estimated_input_tokens: u64,
|
||||
retry_delay_secs: u64,
|
||||
state: Mutex<ObserverState>,
|
||||
}
|
||||
|
||||
impl ShellFullReplaceObserver {
|
||||
pub(crate) fn new(
|
||||
session_id: String,
|
||||
estimated_input_tokens: u64,
|
||||
retry_delay_secs: u64,
|
||||
) -> Self {
|
||||
Self {
|
||||
session_id,
|
||||
estimated_input_tokens,
|
||||
retry_delay_secs,
|
||||
state: Mutex::new(ObserverState::default()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Cumulative number of attempts so far (across all input-ladder stages).
|
||||
/// Read mid-loop to label the `input_overflow` retry event.
|
||||
pub(crate) fn attempt_count(&self) -> u32 {
|
||||
self.state.lock().unwrap().attempts
|
||||
}
|
||||
|
||||
/// Whether any attempt so far produced a degenerate summary — lets the L5
|
||||
/// loop distinguish degenerate-exhausted from empty-exhausted.
|
||||
pub(crate) fn degenerate_seen(&self) -> bool {
|
||||
self.state.lock().unwrap().degenerate_rejections > 0
|
||||
}
|
||||
|
||||
/// The most recent rendered error/diagnostic detail, for `last_error`.
|
||||
pub(crate) fn last_error_message(&self) -> Option<String> {
|
||||
self.state.lock().unwrap().last_error_msg.clone()
|
||||
}
|
||||
|
||||
/// Drain the collected telemetry. The cumulative attempt count spans all
|
||||
/// input-ladder stages because the same observer instance is shared across
|
||||
/// every per-stage call.
|
||||
pub(crate) fn into_telemetry(self) -> FullReplaceTelemetry {
|
||||
let s = self.state.into_inner().unwrap();
|
||||
FullReplaceTelemetry {
|
||||
attempts: s.attempts,
|
||||
attempt_details: s.attempt_details,
|
||||
degenerate_rejections: s.degenerate_rejections,
|
||||
transient_rejections: s.transient_rejections,
|
||||
deterministic_rejections: s.deterministic_rejections,
|
||||
last_rejected_summary: s.last_rejected_summary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FullReplaceObserver for ShellFullReplaceObserver {
|
||||
fn on_attempt(&self, _attempt: u32, outcome: &FullReplaceAttemptOutcome<'_>) {
|
||||
let mut s = self.state.lock().unwrap();
|
||||
// The shared `attempt` resets per ladder stage; keep a cumulative count
|
||||
// so artifact rows match the pre-migration numbering.
|
||||
s.attempts += 1;
|
||||
let attempt = s.attempts;
|
||||
|
||||
match outcome {
|
||||
FullReplaceAttemptOutcome::Success { summary } => {
|
||||
s.attempt_details.push(CompactionAttempt {
|
||||
attempt,
|
||||
outcome: "success".to_string(),
|
||||
summary_chars: summary.chars().count() as u64,
|
||||
summary: None,
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
FullReplaceAttemptOutcome::Degenerate {
|
||||
summary,
|
||||
will_retry,
|
||||
} => {
|
||||
s.degenerate_rejections += 1;
|
||||
let summary_chars = summary.chars().count();
|
||||
s.attempt_details.push(CompactionAttempt {
|
||||
attempt,
|
||||
outcome: "degenerate".to_string(),
|
||||
summary_chars: summary_chars as u64,
|
||||
summary: Some(bound_captured_output(summary, MAX_CAPTURED_SUMMARY_CHARS)),
|
||||
error: None,
|
||||
});
|
||||
s.last_rejected_summary = Some((*summary).to_string());
|
||||
s.last_error_msg = Some(format!(
|
||||
"compact failed: degenerate summary \
|
||||
({summary_chars} chars for ~{} input tokens)",
|
||||
self.estimated_input_tokens
|
||||
));
|
||||
if *will_retry {
|
||||
tracing::warn!(
|
||||
session_id = %self.session_id,
|
||||
attempt,
|
||||
summary_chars,
|
||||
estimated_input_tokens = self.estimated_input_tokens,
|
||||
retry_delay_secs = self.retry_delay_secs,
|
||||
"Compaction produced a degenerate summary, retrying in {} seconds...",
|
||||
self.retry_delay_secs
|
||||
);
|
||||
} else {
|
||||
tracing::error!(
|
||||
session_id = %self.session_id,
|
||||
attempt,
|
||||
summary_chars,
|
||||
estimated_input_tokens = self.estimated_input_tokens,
|
||||
"Compaction produced only degenerate summaries after max retries"
|
||||
);
|
||||
}
|
||||
}
|
||||
FullReplaceAttemptOutcome::EmptyResponse { .. } => {
|
||||
// The shell surfaces an empty response as a transient error
|
||||
// (`generate_session_compact` returns `Transient`), so it never
|
||||
// reaches the shared `Ok("")` branch; handle defensively.
|
||||
s.transient_rejections += 1;
|
||||
let msg = "compact failed: model returned empty response".to_string();
|
||||
s.attempt_details.push(CompactionAttempt {
|
||||
attempt,
|
||||
outcome: "transient".to_string(),
|
||||
summary_chars: 0,
|
||||
summary: None,
|
||||
error: Some(msg.clone()),
|
||||
});
|
||||
s.last_error_msg = Some(msg);
|
||||
}
|
||||
FullReplaceAttemptOutcome::Failure {
|
||||
message,
|
||||
deterministic,
|
||||
context_overflow,
|
||||
will_retry,
|
||||
} => {
|
||||
// A context overflow is recorded as a `deterministic` attempt
|
||||
// (matching the pre-migration row) but does NOT count toward
|
||||
// `deterministic_rejections` — the L5 ladder steps down on it
|
||||
// and tracks its own `input_overflow_rejections`.
|
||||
if *deterministic {
|
||||
if !*context_overflow {
|
||||
s.deterministic_rejections += 1;
|
||||
tracing::error!(
|
||||
session_id = %self.session_id,
|
||||
attempt,
|
||||
error = %message,
|
||||
"Compaction failed (deterministic error class, no further retries)"
|
||||
);
|
||||
}
|
||||
s.attempt_details.push(CompactionAttempt {
|
||||
attempt,
|
||||
outcome: "deterministic".to_string(),
|
||||
summary_chars: 0,
|
||||
summary: None,
|
||||
error: Some((*message).to_string()),
|
||||
});
|
||||
} else {
|
||||
s.transient_rejections += 1;
|
||||
s.attempt_details.push(CompactionAttempt {
|
||||
attempt,
|
||||
outcome: "transient".to_string(),
|
||||
summary_chars: 0,
|
||||
summary: None,
|
||||
error: Some((*message).to_string()),
|
||||
});
|
||||
if *will_retry {
|
||||
tracing::warn!(
|
||||
session_id = %self.session_id,
|
||||
attempt,
|
||||
retry_delay_secs = self.retry_delay_secs,
|
||||
error = %message,
|
||||
"Compaction attempt {} failed, retrying in {} seconds...",
|
||||
attempt,
|
||||
self.retry_delay_secs
|
||||
);
|
||||
} else {
|
||||
tracing::error!(
|
||||
session_id = %self.session_id,
|
||||
attempt,
|
||||
error = %message,
|
||||
"Compaction failed after max retries"
|
||||
);
|
||||
}
|
||||
}
|
||||
s.last_error_msg = Some((*message).to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user