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:
commit
d6c20fc13f
2612 files changed
+1353757
No files matched your search
@@ -0,0 +1,385 @@
|
||||
//! Pure builders for prefire two-pass compaction (shell-only).
|
||||
//!
|
||||
//! Pass1 summarizes ~95% of history (by estimated-token weight) → NOTE₁.
|
||||
//! Pass2 rewrites NOTE₁ + the ~5% tail into the successor-visible NOTE₂.
|
||||
//! Sampling lives in [`super::compaction`]; this module has no I/O.
|
||||
|
||||
use kigi_chat_state::estimate_item_tokens;
|
||||
use kigi_sampling_types::ConversationItem;
|
||||
|
||||
/// Default history fraction covered by pass1; the remainder is the blocking
|
||||
/// pass2 tail, so keep it small (prod pass2 latency is dominated by tail prefill).
|
||||
pub(crate) const TWO_PASS_DEFAULT_SPLIT_FRACTION: f64 = 0.95;
|
||||
|
||||
/// Minimum char length for a closed `<summary>` block to be preferred as NOTE₁
|
||||
/// over the full pass1 response.
|
||||
const TWO_PASS_MIN_SUMMARY_BLOCK_CHARS: usize = 1000;
|
||||
|
||||
/// Cap on NOTE₁ text embedded in pass2 (carrier + special turn).
|
||||
const TWO_PASS_MAX_NOTE1_CHARS: usize = 12_000;
|
||||
|
||||
/// Result of splitting a conversation for two-pass compaction.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct TwoPassSplit<'a> {
|
||||
pub prefix: &'a [ConversationItem],
|
||||
pub tail: &'a [ConversationItem],
|
||||
pub split_idx: usize,
|
||||
}
|
||||
|
||||
/// Choose a split index so prefix weight is at least `fraction` of total.
|
||||
fn split_index_by_token_fraction(weights: &[u64], fraction: f64) -> usize {
|
||||
if weights.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
let frac = fraction.clamp(0.05, 0.95);
|
||||
let total_w = weights.iter().copied().sum::<u64>().max(1);
|
||||
let target_w = frac * total_w as f64;
|
||||
let mut acc = 0u64;
|
||||
let mut split_idx = weights.len().saturating_sub(1).max(1);
|
||||
for (i, w) in weights.iter().enumerate() {
|
||||
acc = acc.saturating_add(*w);
|
||||
if acc as f64 >= target_w {
|
||||
split_idx = (i + 1).max(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if split_idx >= weights.len() && weights.len() > 1 {
|
||||
split_idx = weights.len() - 1;
|
||||
}
|
||||
split_idx
|
||||
}
|
||||
|
||||
/// Never separate an assistant `tool_calls` turn from its following `ToolResult`s.
|
||||
fn snap_split_idx_to_tool_boundaries(
|
||||
conversation: &[ConversationItem],
|
||||
mut split_idx: usize,
|
||||
) -> usize {
|
||||
let n = conversation.len();
|
||||
if n == 0 {
|
||||
return 0;
|
||||
}
|
||||
split_idx = split_idx.min(n);
|
||||
|
||||
while split_idx < n && matches!(conversation[split_idx], ConversationItem::ToolResult(_)) {
|
||||
split_idx += 1;
|
||||
}
|
||||
if split_idx < n
|
||||
&& let ConversationItem::Assistant(a) = &conversation[split_idx]
|
||||
&& !a.tool_calls.is_empty()
|
||||
{
|
||||
split_idx += 1;
|
||||
while split_idx < n && matches!(conversation[split_idx], ConversationItem::ToolResult(_)) {
|
||||
split_idx += 1;
|
||||
}
|
||||
}
|
||||
while split_idx > 0 && split_idx < n {
|
||||
let Some(ConversationItem::Assistant(a)) = conversation.get(split_idx - 1) else {
|
||||
break;
|
||||
};
|
||||
if a.tool_calls.is_empty() {
|
||||
break;
|
||||
}
|
||||
if !matches!(
|
||||
conversation.get(split_idx),
|
||||
Some(ConversationItem::ToolResult(_))
|
||||
) {
|
||||
break;
|
||||
}
|
||||
while split_idx < n && matches!(conversation[split_idx], ConversationItem::ToolResult(_)) {
|
||||
split_idx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if split_idx >= n && n > 1 {
|
||||
let mut candidate = n - 1;
|
||||
while candidate > 1 && matches!(conversation[candidate], ConversationItem::ToolResult(_)) {
|
||||
candidate -= 1;
|
||||
}
|
||||
if candidate > 0
|
||||
&& let ConversationItem::Assistant(a) = &conversation[candidate]
|
||||
&& !a.tool_calls.is_empty()
|
||||
{
|
||||
// candidate at assistant — good for tail start.
|
||||
} else if candidate > 0
|
||||
&& matches!(conversation[candidate], ConversationItem::ToolResult(_))
|
||||
{
|
||||
let mut i = candidate;
|
||||
while i > 0 && matches!(conversation[i], ConversationItem::ToolResult(_)) {
|
||||
i -= 1;
|
||||
}
|
||||
if matches!(
|
||||
conversation.get(i),
|
||||
Some(ConversationItem::Assistant(a)) if !a.tool_calls.is_empty()
|
||||
) {
|
||||
candidate = i;
|
||||
}
|
||||
}
|
||||
if candidate >= 1 && candidate < n {
|
||||
split_idx = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
split_idx.min(n)
|
||||
}
|
||||
|
||||
/// Split `conversation` into pass1 prefix / pass2 tail by estimated-token weight.
|
||||
pub(crate) fn split_conversation_for_two_pass(
|
||||
conversation: &[ConversationItem],
|
||||
split_fraction: f64,
|
||||
) -> TwoPassSplit<'_> {
|
||||
let weights: Vec<u64> = conversation.iter().map(estimate_item_tokens).collect();
|
||||
let mut split_idx = split_index_by_token_fraction(&weights, split_fraction);
|
||||
split_idx = snap_split_idx_to_tool_boundaries(conversation, split_idx);
|
||||
let split_idx = split_idx.min(conversation.len());
|
||||
TwoPassSplit {
|
||||
prefix: &conversation[..split_idx],
|
||||
tail: &conversation[split_idx..],
|
||||
split_idx,
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_summary_block(text: &str, min_chars: usize) -> Option<String> {
|
||||
if text.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let open = "<summary>";
|
||||
let close = "</summary>";
|
||||
let lower = text.to_ascii_lowercase();
|
||||
let mut blocks: Vec<String> = Vec::new();
|
||||
let mut search_from = 0usize;
|
||||
let text_bytes = text.as_bytes();
|
||||
let lower_bytes = lower.as_bytes();
|
||||
while search_from < lower_bytes.len() {
|
||||
let Some(rel) = find_bytes(&lower_bytes[search_from..], open.as_bytes()) else {
|
||||
break;
|
||||
};
|
||||
let start = search_from + rel + open.len();
|
||||
let Some(rel_close) = find_bytes(&lower_bytes[start..], close.as_bytes()) else {
|
||||
break;
|
||||
};
|
||||
let end = start + rel_close;
|
||||
let inner = std::str::from_utf8(&text_bytes[start..end]).unwrap_or("");
|
||||
blocks.push(inner.to_string());
|
||||
search_from = end + close.len();
|
||||
}
|
||||
for block in blocks.into_iter().rev() {
|
||||
let stripped = block.trim();
|
||||
if stripped.chars().count() > min_chars {
|
||||
return Some(stripped.to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
|
||||
haystack.windows(needle.len()).position(|w| w == needle)
|
||||
}
|
||||
|
||||
/// Prefer a substantive `<summary>` inner for NOTE₁; otherwise the full pass1 response.
|
||||
pub(crate) fn note_for_two_pass_pass2(pass1_raw: &str) -> String {
|
||||
let mut note = extract_summary_block(pass1_raw, TWO_PASS_MIN_SUMMARY_BLOCK_CHARS)
|
||||
.unwrap_or_else(|| pass1_raw.trim().to_string());
|
||||
let n = note.chars().count();
|
||||
if n > TWO_PASS_MAX_NOTE1_CHARS {
|
||||
note = note.chars().take(TWO_PASS_MAX_NOTE1_CHARS).collect();
|
||||
note.push_str("\n\n[… NOTE₁ truncated for pass2 input budget …]");
|
||||
}
|
||||
note
|
||||
}
|
||||
|
||||
fn format_two_pass_note1_carrier(note1: &str) -> String {
|
||||
let note1 = note1.trim();
|
||||
format!(
|
||||
"Your conversation was summarized due to context constraints. \
|
||||
Here is the summary of the conversation so far:\n\n\
|
||||
<summary_content>\n{note1}\n</summary_content>\n\n\
|
||||
Continue with the compaction task below."
|
||||
)
|
||||
}
|
||||
|
||||
fn format_two_pass_special_pass2_user(note1: &str, compaction_prompt: &str) -> String {
|
||||
let note1 = note1.trim();
|
||||
let summary_block = format!("<summary_content>\n{note1}\n</summary_content>");
|
||||
let uq = if compaction_prompt.trim().is_empty() {
|
||||
"Please summarize the conversation so far."
|
||||
} else {
|
||||
compaction_prompt
|
||||
};
|
||||
format!(
|
||||
"This is a special compaction case (two-pass / hierarchical summarization).\n\
|
||||
You are writing the *final* compaction note that a successor assistant will \
|
||||
rely on as their only memory of the conversation.\n\n\
|
||||
Critical requirements:\n\
|
||||
- Incorporate the **entire** prior summary below into your final note — do not \
|
||||
omit sections, defer to \"see prior compaction\", or drop early history because \
|
||||
newer turns are in context.\n\
|
||||
- Merge that prior summary with the more recent conversation turns above into \
|
||||
one coherent, faithful, self-contained summary (same structure/sections you \
|
||||
normally use for compaction).\n\
|
||||
- Preserve concrete values, file paths, errors/blockers, operational how-tos, \
|
||||
key findings, and pending tasks from *both* the prior summary and the recent \
|
||||
turns when they still matter.\n\n\
|
||||
Prior summary to incorporate in full (duplicate of the summary_content above):\n\n\
|
||||
{summary_block}\n\n\
|
||||
Compaction instruction:\n\
|
||||
{uq}"
|
||||
)
|
||||
}
|
||||
|
||||
/// Pass1 sample history: `prefix` + compaction instruction user turn.
|
||||
pub(crate) fn build_two_pass_pass1_history(
|
||||
prefix: &[ConversationItem],
|
||||
compaction_prompt: &str,
|
||||
) -> Vec<ConversationItem> {
|
||||
let mut history = prefix.to_vec();
|
||||
history.push(ConversationItem::user(compaction_prompt.to_string()));
|
||||
history
|
||||
}
|
||||
|
||||
/// Pass2 sample history: system (from prefix) + NOTE₁ carrier + tail + special turn.
|
||||
///
|
||||
/// Successor-visible artifact is the model output of *this* history only (NOTE₂).
|
||||
pub(crate) fn build_two_pass_pass2_history(
|
||||
prefix: &[ConversationItem],
|
||||
tail: &[ConversationItem],
|
||||
note1: &str,
|
||||
compaction_prompt: &str,
|
||||
) -> Vec<ConversationItem> {
|
||||
let mut history: Vec<ConversationItem> = Vec::new();
|
||||
|
||||
for item in prefix {
|
||||
if matches!(item, ConversationItem::System(_)) {
|
||||
history.push(item.clone());
|
||||
}
|
||||
}
|
||||
if !history
|
||||
.iter()
|
||||
.any(|i| matches!(i, ConversationItem::System(_)))
|
||||
{
|
||||
history.push(ConversationItem::system("You are a helpful assistant."));
|
||||
}
|
||||
|
||||
history.push(ConversationItem::user(format_two_pass_note1_carrier(note1)));
|
||||
history.extend(tail.iter().cloned());
|
||||
history.push(ConversationItem::user(format_two_pass_special_pass2_user(
|
||||
note1,
|
||||
compaction_prompt,
|
||||
)));
|
||||
history
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn split_leaves_tail_when_possible() {
|
||||
let weights = vec![10u64, 10, 10, 10, 10];
|
||||
let idx = split_index_by_token_fraction(&weights, 0.9);
|
||||
assert!(idx < weights.len());
|
||||
assert!(idx >= 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_split_fraction_leaves_five_percent_tail() {
|
||||
assert_eq!(TWO_PASS_DEFAULT_SPLIT_FRACTION, 0.95);
|
||||
// With the default fraction a non-empty tail must always survive the
|
||||
// split (pass2 needs recent turns to rewrite against NOTE₁).
|
||||
let weights = vec![10u64; 40];
|
||||
let idx = split_index_by_token_fraction(&weights, TWO_PASS_DEFAULT_SPLIT_FRACTION);
|
||||
assert_eq!(idx, 38); // 38/40 = 95% by weight
|
||||
assert!(idx < weights.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_does_not_sever_tool_pairs() {
|
||||
use kigi_sampling_types::ToolCall;
|
||||
let mut assistant = ConversationItem::assistant("call");
|
||||
if let ConversationItem::Assistant(a) = &mut assistant {
|
||||
a.tool_calls.push(ToolCall {
|
||||
id: "tc1".into(),
|
||||
name: "bash".into(),
|
||||
arguments: "{}".into(),
|
||||
});
|
||||
}
|
||||
let conv = vec![
|
||||
ConversationItem::user("a".repeat(400)),
|
||||
ConversationItem::assistant("b".repeat(400)),
|
||||
assistant,
|
||||
ConversationItem::tool_result("tc1", "ok"),
|
||||
ConversationItem::user("tail"),
|
||||
];
|
||||
let split = split_conversation_for_two_pass(&conv, 0.9);
|
||||
if let Some(ConversationItem::Assistant(a)) = split.prefix.last()
|
||||
&& !a.tool_calls.is_empty()
|
||||
{
|
||||
assert!(!matches!(
|
||||
split.tail.first(),
|
||||
Some(ConversationItem::ToolResult(_))
|
||||
));
|
||||
}
|
||||
assert!(!matches!(
|
||||
split.tail.first(),
|
||||
Some(ConversationItem::ToolResult(_))
|
||||
));
|
||||
let prefix_has_call = split.prefix.iter().any(|i| {
|
||||
matches!(i, ConversationItem::Assistant(a) if a.tool_calls.iter().any(|t| t.id.as_ref() == "tc1"))
|
||||
});
|
||||
let prefix_has_result = split
|
||||
.prefix
|
||||
.iter()
|
||||
.any(|i| matches!(i, ConversationItem::ToolResult(t) if t.tool_call_id == "tc1"));
|
||||
let tail_has_call = split.tail.iter().any(|i| {
|
||||
matches!(i, ConversationItem::Assistant(a) if a.tool_calls.iter().any(|t| t.id.as_ref() == "tc1"))
|
||||
});
|
||||
let tail_has_result = split
|
||||
.tail
|
||||
.iter()
|
||||
.any(|i| matches!(i, ConversationItem::ToolResult(t) if t.tool_call_id == "tc1"));
|
||||
assert_eq!(prefix_has_call, prefix_has_result);
|
||||
assert_eq!(tail_has_call, tail_has_result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn note_prefers_summary_block_and_caps_huge_raw() {
|
||||
let text = format!(
|
||||
"<summary>short</summary>\n<summary>\n{}\n</summary>",
|
||||
"x".repeat(1001)
|
||||
);
|
||||
assert_eq!(note_for_two_pass_pass2(&text), "x".repeat(1001));
|
||||
assert_eq!(note_for_two_pass_pass2("no tags"), "no tags");
|
||||
let huge = "n".repeat(TWO_PASS_MAX_NOTE1_CHARS + 500);
|
||||
let note = note_for_two_pass_pass2(&huge);
|
||||
assert!(note.chars().count() <= TWO_PASS_MAX_NOTE1_CHARS + 80);
|
||||
assert!(note.contains("truncated"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pass_histories_shape() {
|
||||
let conv = vec![
|
||||
ConversationItem::system("You are Grok."),
|
||||
ConversationItem::user("early"),
|
||||
ConversationItem::assistant("early-a"),
|
||||
ConversationItem::user("late"),
|
||||
ConversationItem::assistant("late-a"),
|
||||
];
|
||||
let split = split_conversation_for_two_pass(&conv, 0.5);
|
||||
let prompt = "1. Primary Request and Intent: x\n5. Optional Next Step: y\n";
|
||||
let pass1 = build_two_pass_pass1_history(split.prefix, prompt);
|
||||
assert!(matches!(pass1.last(), Some(ConversationItem::User(_))));
|
||||
|
||||
let note1 = "x".repeat(1001);
|
||||
let pass2 = build_two_pass_pass2_history(split.prefix, split.tail, ¬e1, prompt);
|
||||
assert!(pass2.iter().any(|i| matches!(
|
||||
i,
|
||||
ConversationItem::System(s) if s.content.as_ref() == "You are Grok."
|
||||
)));
|
||||
let texts: Vec<String> = pass2.iter().map(|i| i.text_content()).collect();
|
||||
assert!(texts.iter().any(|t| t.contains("<summary_content>")));
|
||||
assert!(
|
||||
texts
|
||||
.last()
|
||||
.is_some_and(|t| t.contains("special compaction case"))
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user