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,233 @@
|
||||
//! Applies a model switch to a session — the ungated path. `set_session_model`
|
||||
//! enforces the `allowed_models` gate before delegating here; internal callers
|
||||
//! (`new_session`, `load_session`) call `apply` directly.
|
||||
use crate::agent::config;
|
||||
use crate::agent::mvp_agent::{
|
||||
MvpAgent, agent_name_after_model_switch, harnesses_are_compatible, resolve_required_agent_type,
|
||||
};
|
||||
use crate::session::SessionCommand;
|
||||
use agent_client_protocol::{self as acp};
|
||||
use kigi_sampling_types::parse_reasoning_effort_meta;
|
||||
use tokio::sync::oneshot;
|
||||
/// Apply a model switch to a session (no gate — `set_session_model` gates first).
|
||||
pub(crate) async fn apply(
|
||||
agent: &MvpAgent,
|
||||
args: acp::SetSessionModelRequest,
|
||||
) -> Result<acp::SetSessionModelResponse, acp::Error> {
|
||||
tracing::info!("Received set session model request {args:?}");
|
||||
kigi_log::unified_log::info(
|
||||
"model changed",
|
||||
Some(args.session_id.0.as_ref()),
|
||||
Some(serde_json::json!({ "model" : args.model_id.0.as_ref() })),
|
||||
);
|
||||
tracing::debug!("session_session_model::mvp_agent: {:?}", &args);
|
||||
let effort_override = parse_reasoning_effort_meta(args.meta.as_ref());
|
||||
let acp::SetSessionModelRequest {
|
||||
session_id,
|
||||
model_id,
|
||||
..
|
||||
} = args;
|
||||
let handle = agent
|
||||
.session_handle_waiting_for_load(&session_id)
|
||||
.await
|
||||
.ok_or_else(|| acp::Error::invalid_params().data("unknown session id"))?;
|
||||
let model = agent.resolve_model_id(&model_id)?;
|
||||
let use_concise = model.info().use_concise;
|
||||
let session_default = handle
|
||||
.session_default_agent_profile
|
||||
.as_deref()
|
||||
.unwrap_or(&handle.agent_name);
|
||||
let required_agent_type =
|
||||
resolve_required_agent_type(Some(model.info().agent_type.as_str()), session_default);
|
||||
let previous_model_id = handle.model_id.0.clone();
|
||||
let mut pending_rebuild_definition: Option<kigi_agent::AgentDefinition> = None;
|
||||
{
|
||||
let required = &required_agent_type;
|
||||
let turn_count = handle
|
||||
.signals_handle
|
||||
.snapshot()
|
||||
.await
|
||||
.map(|s| s.turn_count)
|
||||
.unwrap_or(0);
|
||||
let (agent_tx, agent_rx) = oneshot::channel();
|
||||
let _ = handle.cmd_tx.send(SessionCommand::GetActiveAgent {
|
||||
responds_to: agent_tx,
|
||||
});
|
||||
let active_agent_type = agent_rx.await.ok().flatten();
|
||||
let is_mismatch = active_agent_type
|
||||
.as_ref()
|
||||
.is_some_and(|active| !harnesses_are_compatible(active, required));
|
||||
tracing::info!(
|
||||
session_id = % session_id.0, model_id = % model_id.0, ? required_agent_type,
|
||||
? active_agent_type, turn_count, is_mismatch,
|
||||
"set_session_model: agent type compatibility check"
|
||||
);
|
||||
if is_mismatch && turn_count > 0 {
|
||||
tracing::warn!(
|
||||
session_id = % session_id.0, model_id = % model_id.0, active_agent = ?
|
||||
active_agent_type, required_agent = % required, turn_count,
|
||||
"set_session_model: agent type mismatch rejected"
|
||||
);
|
||||
let err_payload = config::ModelSwitchIncompatibleAgentError {
|
||||
code: config::MODEL_SWITCH_INCOMPATIBLE_AGENT.to_string(),
|
||||
active_agent_type: active_agent_type.unwrap_or_else(|| "unknown".to_owned()),
|
||||
required_agent_type: required.clone(),
|
||||
model_id: model_id.0.to_string(),
|
||||
suggestion: "start_new_session".to_string(),
|
||||
};
|
||||
return Err(err_payload.into_acp_error());
|
||||
}
|
||||
if is_mismatch && turn_count == 0 {
|
||||
let cwd = handle.tool_context.cwd.as_path();
|
||||
let resolved = kigi_agent::discovery::by_name_in_cwd_with_plugins(
|
||||
required,
|
||||
cwd,
|
||||
agent.plugin_registry_handle.snapshot().as_deref(),
|
||||
);
|
||||
match resolved {
|
||||
Some(def) => {
|
||||
tracing::info!(
|
||||
session_id = % session_id.0, model_id = % model_id.0,
|
||||
required_agent_type = % required, agent_def_name = % def.name,
|
||||
"set_session_model: zero-turn harness switch — queued agent rebuild"
|
||||
);
|
||||
pending_rebuild_definition = Some(def);
|
||||
}
|
||||
None => {
|
||||
tracing::warn!(
|
||||
session_id = % session_id.0, model_id = % model_id.0,
|
||||
required_agent_type = % required,
|
||||
"set_session_model: zero-turn harness switch — could not resolve agent definition; proceeding with stale harness"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut model_sampling =
|
||||
agent.prepare_sampling_config_for_model(&model, handle.origin_client.clone());
|
||||
if let Some(eff) = effort_override {
|
||||
if agent
|
||||
.models_manager
|
||||
.model_supports_reasoning_effort(model_id.0.as_ref())
|
||||
{
|
||||
tracing::info!(
|
||||
session_id = % session_id.0, effort = % eff,
|
||||
"set_session_model: applying reasoning_effort override from meta"
|
||||
);
|
||||
model_sampling.reasoning_effort = Some(eff);
|
||||
} else {
|
||||
tracing::warn!(
|
||||
session_id = % session_id.0, model_id = % model_id.0, effort = % eff,
|
||||
"set_session_model: ignoring reasoning_effort override — model does not support it"
|
||||
);
|
||||
}
|
||||
}
|
||||
let applied_effort = model_sampling.reasoning_effort;
|
||||
let gate_closed = !handle
|
||||
.gateway_enabled
|
||||
.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let apply_prompt_override = !gate_closed;
|
||||
if gate_closed {
|
||||
tracing::info!(
|
||||
session_id = % session_id.0, model_id = % model_id.0,
|
||||
"set_session_model: gateway gate closed, prompt override suppressed"
|
||||
);
|
||||
pending_rebuild_definition = None;
|
||||
}
|
||||
let did_rebuild = if let Some(def) = pending_rebuild_definition {
|
||||
let (rebuild_tx, rebuild_rx) = oneshot::channel();
|
||||
let _ = handle
|
||||
.cmd_tx
|
||||
.send(SessionCommand::RebuildAgentForDefinition {
|
||||
definition: def,
|
||||
responds_to: rebuild_tx,
|
||||
});
|
||||
let rebuild_result = rebuild_rx
|
||||
.await
|
||||
.map_err(|_| acp::Error::internal_error().data("rebuild_agent: actor closed"))?;
|
||||
match rebuild_result {
|
||||
Ok(()) => true,
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
session_id = % session_id.0, model_id = % model_id.0, error = ? e,
|
||||
"set_session_model: zero-turn harness rebuild failed; aborting model switch"
|
||||
);
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let model_unchanged = previous_model_id == model_id.0;
|
||||
let new_threshold = {
|
||||
let cfg = agent.cfg.borrow();
|
||||
let models = agent.models_manager.models();
|
||||
let model = config::find_model_by_id(&models, model_sampling.model.as_str());
|
||||
crate::util::config::resolve_auto_compact_threshold_percent(
|
||||
&cfg,
|
||||
model_sampling.model.as_str(),
|
||||
model.map(|e| &e.info),
|
||||
)
|
||||
};
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let _ = handle.cmd_tx.send(SessionCommand::SetSessionModel {
|
||||
sampling_config: model_sampling,
|
||||
use_concise,
|
||||
apply_prompt_override,
|
||||
skip_prompt_rewrite: did_rebuild || model_unchanged,
|
||||
auto_compact_threshold_percent: new_threshold,
|
||||
responds_to: tx,
|
||||
});
|
||||
let updated_model = rx
|
||||
.await
|
||||
.map_err(|_| acp::Error::internal_error().data("failed to set session model"))?;
|
||||
if let Some(handle) = agent.sessions.borrow_mut().get_mut(&session_id) {
|
||||
handle.model_id = model_id.clone();
|
||||
handle.reasoning_effort = applied_effort;
|
||||
handle.agent_name =
|
||||
agent_name_after_model_switch(did_rebuild, &required_agent_type, &handle.agent_name);
|
||||
}
|
||||
broadcast_model_changed(
|
||||
agent,
|
||||
&session_id,
|
||||
model_id.0.as_ref(),
|
||||
applied_effort.map(|eff| eff.to_string()),
|
||||
);
|
||||
if agent.cfg.borrow().mode != config::AgentMode::Leader {
|
||||
agent.models_manager.set_current_model_id(model_id);
|
||||
agent
|
||||
.models_manager
|
||||
.set_current_reasoning_effort(applied_effort);
|
||||
}
|
||||
Ok(acp::SetSessionModelResponse::new().meta(
|
||||
serde_json::json!({ "model" : updated_model, })
|
||||
.as_object()
|
||||
.cloned(),
|
||||
))
|
||||
}
|
||||
/// Broadcast a `ModelChanged` to every client subscribed to this session so
|
||||
/// followers mirror the new model. The originating client ignores its own echo
|
||||
/// (gated by `model_switch_pending`). Broadcast-only — no eventId, not persisted.
|
||||
fn broadcast_model_changed(
|
||||
agent: &MvpAgent,
|
||||
session_id: &acp::SessionId,
|
||||
model_id: &str,
|
||||
reasoning_effort: Option<String>,
|
||||
) {
|
||||
let notification = crate::extensions::notification::SessionNotification {
|
||||
session_id: session_id.clone(),
|
||||
update: crate::extensions::notification::SessionUpdate::ModelChanged {
|
||||
model_id: model_id.to_owned(),
|
||||
reasoning_effort,
|
||||
},
|
||||
meta: None,
|
||||
};
|
||||
if let Ok(params) = serde_json::value::to_raw_value(¬ification) {
|
||||
agent
|
||||
.gateway
|
||||
.forward_fire_and_forget(acp::ExtNotification::new(
|
||||
"x.ai/session_notification",
|
||||
params.into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user