The PRD's first acceptance gate now holds: grep -RinE '\bx\.ai\b|grok' crates/ --include='*.rs' → 0 matches (exempt: NOTICE and third-party license archives, README provenance, and the required 'Based on Grok Build Open Source' attribution, now sourced from version_attribution.txt). Wire-visible renames (both sides in this repo, changed in lockstep): - Auth method id 'grok.com' → 'kimi-code' (AuthMethodKind::KimiCode). - Every x.ai/* and _x.ai/* ACP ext method and meta key → kigi/* / _kigi/* (~200 names; grokShell → kigiShell). Session-file replay keeps a read-side alias for the legacy '_x.ai/session/update' method so existing updates.jsonl histories load; writes emit only the new name (both directions test-pinned). - Agent types grok-build* → kigi* with a documented legacy-prefix alias at resolution time so persisted sessions keep resolving. - ToolNamespace/BuiltinAgentName GrokBuild* → Kigi* (wire snake_case kigi/kigi_concise/kigi_hashline; schema regenerated); grok_build implementation dirs renamed to kigi*. - x-grok-* headers → x-kigi-*, __GROK_* sentinels → __KIGI_*, themes grokday/groknight → kigiday/kiginight (old persisted values fall back to the default theme), web_fetch allowlist xAI hosts → kimi.com + moonshot platforms, changelog CDN → this repo, grok-build changelog archives deleted. - BYOK default endpoint removed: [endpoints] api_base_url is now truly optional with NO default — consumers fail fast with the flag name when unset (no silent x.ai egress). Mock harnesses inject it explicitly. - System-prompt identity fixed: 'released by xAI' → 'an unofficial community CLI for Kimi' (template + regenerated encrypted form). Also repaired pre-existing grok-era test debt found by the sweep: the stale trace_classify default-model pin, the grok-pager UA label test, pty-harness stale-binary reuse and non-hermetic moonshot routing (a PTY test could previously reach the real api.moonshot.cn), and the outdated oauth fixture scope key. Gates: §9 grep 0; fmt clean; workspace check/clippy 0/0 (-D warnings); FULL cargo test --workspace: 234 suites, 21,961 passed, 0 failed; deny advisories ok.
520 lines
17 KiB
Rust
520 lines
17 KiB
Rust
//! Shared post-compaction reminder helpers (host-agnostic).
|
|
//!
|
|
//! Lives at the crate root rather than under a compaction-style submodule
|
|
//! because it is consumed by *both* compaction styles and both harnesses:
|
|
//!
|
|
//! - Kigi chat intra FullReplace ([`crate::intra_compaction`]) and inter
|
|
//! (appends after sampling via [`append_reminder_block`])
|
|
//! - kigi full-replace ([`crate::code_compaction`] assemble's
|
|
//! `system_reminder`)
|
|
//!
|
|
//! **What lives here:** pure formatting of the three **common** active-agent
|
|
//! sections — Running Background Tasks, TODO List, Running Subagents — plus
|
|
//! `<system-reminder>` wrapping and summary append.
|
|
//!
|
|
//! **What stays in the product host:** snapshotting, tool-name resolution, and harness-only
|
|
//! sections (files, AGENTS.md, skills, MCP, memory). Callers pass **borrowed
|
|
//! views** (`&str` over live state) so long fields (commands, todo content,
|
|
//! descriptions, ids) are not cloned just to format.
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Borrowed views over harness live state (no long-string clones)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Model-facing poll/cancel tool names from the current toolset.
|
|
/// Never hard-code: a client manifest can rename them.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub struct SubagentToolNames<'a> {
|
|
pub poll: &'a str,
|
|
pub cancel: &'a str,
|
|
}
|
|
|
|
/// Status of a todo item in the post-compaction reminder.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum TodoStatus {
|
|
Pending,
|
|
InProgress,
|
|
Completed,
|
|
Cancelled,
|
|
}
|
|
|
|
impl TodoStatus {
|
|
pub fn is_actionable(self) -> bool {
|
|
matches!(self, Self::Pending | Self::InProgress)
|
|
}
|
|
|
|
pub fn tag(self) -> &'static str {
|
|
match self {
|
|
Self::Pending => "[pending]",
|
|
Self::InProgress => "[in_progress]",
|
|
Self::Completed => "[completed]",
|
|
Self::Cancelled => "[cancelled]",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub struct TodoItem<'a> {
|
|
pub id: &'a str,
|
|
pub content: &'a str,
|
|
pub status: TodoStatus,
|
|
}
|
|
|
|
/// Still-running background task. `task_id` is rendered verbatim.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub struct BackgroundTask<'a> {
|
|
pub task_id: &'a str,
|
|
pub command: &'a str,
|
|
/// Parenthetical status (typically `"running"`).
|
|
pub status: &'a str,
|
|
pub tool_name: Option<&'a str>,
|
|
}
|
|
|
|
/// Still-running sub-agent. `subagent_id` is rendered verbatim.
|
|
///
|
|
/// `subagent_type` / `description` are optional so chat (no type, optional
|
|
/// desc) and build (both present) share one line format.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub struct RunningSubagent<'a> {
|
|
pub subagent_id: &'a str,
|
|
pub subagent_type: Option<&'a str>,
|
|
pub description: Option<&'a str>,
|
|
pub elapsed_secs: u64,
|
|
}
|
|
|
|
/// Borrowed active-agent state for reminder rendering.
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct ActiveAgentReminderState<'a> {
|
|
pub running_commands: &'a [BackgroundTask<'a>],
|
|
pub todos: &'a [TodoItem<'a>],
|
|
pub running_subagents: &'a [RunningSubagent<'a>],
|
|
}
|
|
|
|
impl ActiveAgentReminderState<'_> {
|
|
pub fn is_empty(&self) -> bool {
|
|
self.running_commands.is_empty()
|
|
&& self.running_subagents.is_empty()
|
|
&& !self.has_actionable_todos()
|
|
}
|
|
|
|
pub fn has_actionable_todos(&self) -> bool {
|
|
self.todos.iter().any(|t| t.status.is_actionable())
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Section formatters
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// `## Running Background Tasks`, or `None` when empty.
|
|
pub fn section_background_tasks(tasks: &[BackgroundTask<'_>]) -> Option<String> {
|
|
if tasks.is_empty() {
|
|
return None;
|
|
}
|
|
let lines = tasks
|
|
.iter()
|
|
.map(|t| match t.tool_name {
|
|
Some(tool) => format!(
|
|
"- \"{}\": `{}` ({}, {})",
|
|
t.task_id, t.command, t.status, tool
|
|
),
|
|
None => format!("- \"{}\": `{}` ({})", t.task_id, t.command, t.status),
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
Some(format!(
|
|
"## Running Background Tasks\n\
|
|
These tasks are still running:\n{lines}"
|
|
))
|
|
}
|
|
|
|
/// `## TODO List` for actionable items, or `None` when none. Completed/
|
|
/// cancelled collapse to a count trailer.
|
|
pub fn section_todo_list(todos: &[TodoItem<'_>]) -> Option<String> {
|
|
let active: Vec<_> = todos
|
|
.iter()
|
|
.filter(|t| t.status.is_actionable())
|
|
.map(|t| format!("- {} {}: {}", t.status.tag(), t.id, t.content))
|
|
.collect();
|
|
if active.is_empty() {
|
|
return None;
|
|
}
|
|
let completed = todos
|
|
.iter()
|
|
.filter(|t| t.status == TodoStatus::Completed)
|
|
.count();
|
|
let cancelled = todos
|
|
.iter()
|
|
.filter(|t| t.status == TodoStatus::Cancelled)
|
|
.count();
|
|
let trailer = match (completed, cancelled) {
|
|
(0, 0) => String::new(),
|
|
(c, 0) => format!("\n({c} completed)"),
|
|
(0, k) => format!("\n({k} cancelled)"),
|
|
(c, k) => format!("\n({c} completed, {k} cancelled)"),
|
|
};
|
|
Some(format!(
|
|
"## TODO List\n\
|
|
This is your task list from before the conversation was compacted — it is still \
|
|
active. Keep working through the items below and update their status as you make \
|
|
progress:\n{}{trailer}",
|
|
active.join("\n"),
|
|
))
|
|
}
|
|
|
|
/// `## Running Subagents`, or `None` when empty. Omit entirely when tool
|
|
/// names cannot be resolved rather than point at wrong names.
|
|
pub fn section_running_subagents(
|
|
subagents: &[RunningSubagent<'_>],
|
|
tools: &SubagentToolNames<'_>,
|
|
) -> Option<String> {
|
|
if subagents.is_empty() {
|
|
return None;
|
|
}
|
|
let lines = subagents
|
|
.iter()
|
|
.map(format_subagent_line)
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
Some(format!(
|
|
"## Running Subagents\n\
|
|
These subagents were launched before this compaction and are still running. \
|
|
Use `{}` with the subagent_id to check their status or retrieve results. \
|
|
Use `{}` with the subagent_id to cancel a subagent.\n{lines}",
|
|
tools.poll, tools.cancel
|
|
))
|
|
}
|
|
|
|
fn format_subagent_line(s: &RunningSubagent<'_>) -> String {
|
|
let mut head = format!("subagent_id: `{}`", s.subagent_id);
|
|
if let Some(ty) = s.subagent_type {
|
|
head.push_str(", type: `");
|
|
head.push_str(ty);
|
|
head.push('`');
|
|
}
|
|
if let Some(desc) = s.description {
|
|
head.push_str(", task: \"");
|
|
head.push_str(desc);
|
|
head.push('"');
|
|
}
|
|
format!("- {head} (running for {}s)", s.elapsed_secs)
|
|
}
|
|
|
|
/// Common sections in order: Background Tasks → TODO → Subagents.
|
|
/// Empty kinds omitted; subagents also omitted when `subagent_tools` is `None`.
|
|
pub fn format_active_agent_sections(
|
|
state: &ActiveAgentReminderState<'_>,
|
|
subagent_tools: Option<&SubagentToolNames<'_>>,
|
|
) -> Vec<String> {
|
|
let mut sections = Vec::with_capacity(3);
|
|
if let Some(s) = section_background_tasks(state.running_commands) {
|
|
sections.push(s);
|
|
}
|
|
if let Some(s) = section_todo_list(state.todos) {
|
|
sections.push(s);
|
|
}
|
|
if let Some(tools) = subagent_tools
|
|
&& let Some(s) = section_running_subagents(state.running_subagents, tools)
|
|
{
|
|
sections.push(s);
|
|
}
|
|
sections
|
|
}
|
|
|
|
/// Wrap non-empty sections in `<system-reminder>…</system-reminder>`.
|
|
pub fn wrap_system_reminder(sections: impl IntoIterator<Item = impl AsRef<str>>) -> Option<String> {
|
|
let mut body = String::new();
|
|
for s in sections {
|
|
let s = s.as_ref();
|
|
if s.trim().is_empty() {
|
|
continue;
|
|
}
|
|
if !body.is_empty() {
|
|
body.push_str("\n\n");
|
|
}
|
|
body.push_str(s);
|
|
}
|
|
if body.is_empty() {
|
|
None
|
|
} else {
|
|
Some(format!("<system-reminder>\n{body}\n</system-reminder>"))
|
|
}
|
|
}
|
|
|
|
/// Full active-agent-state `<system-reminder>`, or `None` when nothing to preserve.
|
|
pub fn format_active_agent_reminder(
|
|
state: &ActiveAgentReminderState<'_>,
|
|
subagent_tools: Option<&SubagentToolNames<'_>>,
|
|
) -> Option<String> {
|
|
wrap_system_reminder(format_active_agent_sections(state, subagent_tools))
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Summary injection
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Append a trailing block to a compaction summary, separated by a blank line.
|
|
/// Returns `summary` unchanged when `reminder` is `None` or blank.
|
|
pub fn append_reminder_block(summary: String, reminder: Option<&str>) -> String {
|
|
match reminder {
|
|
Some(reminder) if !reminder.trim().is_empty() => format!("{summary}\n\n{reminder}"),
|
|
_ => summary,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn tools_native() -> SubagentToolNames<'static> {
|
|
SubagentToolNames {
|
|
poll: "get_task_output",
|
|
cancel: "kill_task",
|
|
}
|
|
}
|
|
|
|
fn tools_renamed() -> SubagentToolNames<'static> {
|
|
SubagentToolNames {
|
|
poll: "get_command_or_subagent_output",
|
|
cancel: "kill_command_or_subagent",
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn empty_state_is_none() {
|
|
assert!(
|
|
format_active_agent_reminder(
|
|
&ActiveAgentReminderState::default(),
|
|
Some(&tools_native())
|
|
)
|
|
.is_none()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn missing_tool_names_omits_subagent_section_only() {
|
|
let agents = [RunningSubagent {
|
|
subagent_id: "sa-1",
|
|
subagent_type: None,
|
|
description: Some("x"),
|
|
elapsed_secs: 1,
|
|
}];
|
|
let state = ActiveAgentReminderState {
|
|
running_subagents: &agents,
|
|
..Default::default()
|
|
};
|
|
assert!(format_active_agent_reminder(&state, None).is_none());
|
|
|
|
let cmds = [BackgroundTask {
|
|
task_id: "bg-1",
|
|
command: "npm run dev",
|
|
status: "running",
|
|
tool_name: Some("run_terminal_command"),
|
|
}];
|
|
let state = ActiveAgentReminderState {
|
|
running_commands: &cmds,
|
|
..Default::default()
|
|
};
|
|
let out = format_active_agent_reminder(&state, None).expect("reminder");
|
|
assert!(out.contains("## Running Background Tasks"));
|
|
assert!(!out.contains("## Running Subagents"));
|
|
}
|
|
|
|
#[test]
|
|
fn renders_chat_style_subagent_ids_verbatim() {
|
|
let agents = [
|
|
RunningSubagent {
|
|
subagent_id: "019ea7f0-cb66-7aa2-9a09-488a3a795795",
|
|
subagent_type: None,
|
|
description: Some("deploy staging"),
|
|
elapsed_secs: 42,
|
|
},
|
|
RunningSubagent {
|
|
subagent_id: "sa-2",
|
|
subagent_type: None,
|
|
description: None,
|
|
elapsed_secs: 5,
|
|
},
|
|
];
|
|
let state = ActiveAgentReminderState {
|
|
running_subagents: &agents,
|
|
..Default::default()
|
|
};
|
|
let out = format_active_agent_reminder(&state, Some(&tools_native())).expect("reminder");
|
|
assert!(out.starts_with("<system-reminder>"));
|
|
assert!(out.ends_with("</system-reminder>"));
|
|
assert!(out.contains("subagent_id: `019ea7f0-cb66-7aa2-9a09-488a3a795795`"));
|
|
assert!(out.contains("task: \"deploy staging\" (running for 42s)"));
|
|
assert!(out.contains("subagent_id: `sa-2` (running for 5s)"));
|
|
assert!(!out.contains("task-019ea7f0"));
|
|
assert!(!out.contains("type:"));
|
|
}
|
|
|
|
#[test]
|
|
fn renders_build_style_subagent_with_type() {
|
|
let agents = [RunningSubagent {
|
|
subagent_id: "sub-1",
|
|
subagent_type: Some("explore"),
|
|
description: Some("find files"),
|
|
elapsed_secs: 5,
|
|
}];
|
|
let state = ActiveAgentReminderState {
|
|
running_subagents: &agents,
|
|
..Default::default()
|
|
};
|
|
let out = format_active_agent_reminder(&state, Some(&tools_renamed())).expect("reminder");
|
|
assert!(out.contains(
|
|
"- subagent_id: `sub-1`, type: `explore`, task: \"find files\" (running for 5s)"
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn uses_renamed_tool_names_verbatim() {
|
|
let agents = [RunningSubagent {
|
|
subagent_id: "sa-1",
|
|
subagent_type: None,
|
|
description: Some("x"),
|
|
elapsed_secs: 1,
|
|
}];
|
|
let state = ActiveAgentReminderState {
|
|
running_subagents: &agents,
|
|
..Default::default()
|
|
};
|
|
let out = format_active_agent_reminder(&state, Some(&tools_renamed())).expect("reminder");
|
|
assert!(out.contains("get_command_or_subagent_output"));
|
|
assert!(!out.contains("get_task_output"));
|
|
}
|
|
|
|
#[test]
|
|
fn renders_background_tasks() {
|
|
let cmds = [
|
|
BackgroundTask {
|
|
task_id: "019f1723-a9f0-76f2-98ae-56af965922f6",
|
|
command: "npm run dev",
|
|
status: "running",
|
|
tool_name: Some("run_terminal_command"),
|
|
},
|
|
BackgroundTask {
|
|
task_id: "bg-2",
|
|
command: "cargo watch -x test",
|
|
status: "running",
|
|
tool_name: None,
|
|
},
|
|
];
|
|
let state = ActiveAgentReminderState {
|
|
running_commands: &cmds,
|
|
..Default::default()
|
|
};
|
|
let out = format_active_agent_reminder(&state, None).expect("reminder");
|
|
assert!(out.contains(
|
|
"- \"019f1723-a9f0-76f2-98ae-56af965922f6\": `npm run dev` (running, run_terminal_command)"
|
|
));
|
|
assert!(out.contains("- \"bg-2\": `cargo watch -x test` (running)"));
|
|
}
|
|
|
|
#[test]
|
|
fn renders_todo_list_without_tool_names() {
|
|
let todos = [
|
|
TodoItem {
|
|
id: "1",
|
|
content: "scaffold the app",
|
|
status: TodoStatus::Completed,
|
|
},
|
|
TodoItem {
|
|
id: "2",
|
|
content: "wire the API",
|
|
status: TodoStatus::InProgress,
|
|
},
|
|
TodoItem {
|
|
id: "3",
|
|
content: "write tests",
|
|
status: TodoStatus::Pending,
|
|
},
|
|
];
|
|
let state = ActiveAgentReminderState {
|
|
todos: &todos,
|
|
..Default::default()
|
|
};
|
|
let out = format_active_agent_reminder(&state, None).expect("reminder");
|
|
assert!(out.contains("- [in_progress] 2: wire the API"));
|
|
assert!(out.contains("- [pending] 3: write tests"));
|
|
assert!(out.contains("(1 completed)"));
|
|
assert!(!out.contains("scaffold the app"));
|
|
}
|
|
|
|
#[test]
|
|
fn only_completed_todos_is_none() {
|
|
let todos = [TodoItem {
|
|
id: "1",
|
|
content: "done",
|
|
status: TodoStatus::Completed,
|
|
}];
|
|
let state = ActiveAgentReminderState {
|
|
todos: &todos,
|
|
..Default::default()
|
|
};
|
|
assert!(format_active_agent_reminder(&state, None).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn section_order_background_todo_subagent() {
|
|
let cmds = [BackgroundTask {
|
|
task_id: "t1",
|
|
command: "npm run dev",
|
|
status: "running",
|
|
tool_name: None,
|
|
}];
|
|
let todos = [TodoItem {
|
|
id: "2",
|
|
content: "wire the API",
|
|
status: TodoStatus::InProgress,
|
|
}];
|
|
let agents = [RunningSubagent {
|
|
subagent_id: "sa-1",
|
|
subagent_type: None,
|
|
description: Some("deploy staging"),
|
|
elapsed_secs: 1,
|
|
}];
|
|
let state = ActiveAgentReminderState {
|
|
running_commands: &cmds,
|
|
todos: &todos,
|
|
running_subagents: &agents,
|
|
};
|
|
let out = format_active_agent_reminder(&state, Some(&tools_native())).expect("reminder");
|
|
let bg = out.find("## Running Background Tasks").expect("bg");
|
|
let todo = out.find("## TODO List").expect("todo");
|
|
let sub = out.find("## Running Subagents").expect("sub");
|
|
assert!(bg < todo && todo < sub, "order wrong:\n{out}");
|
|
}
|
|
|
|
#[test]
|
|
fn wrap_system_reminder_joins_and_skips_blank() {
|
|
let out = wrap_system_reminder(["## A\nx", "", " ", "## B\ny"]).expect("wrapped");
|
|
assert_eq!(
|
|
out,
|
|
"<system-reminder>\n## A\nx\n\n## B\ny\n</system-reminder>"
|
|
);
|
|
assert!(wrap_system_reminder(std::iter::empty::<&str>()).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn appends_after_blank_line() {
|
|
assert_eq!(
|
|
append_reminder_block("SUMMARY".to_string(), Some("REMINDER")),
|
|
"SUMMARY\n\nREMINDER"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn append_noop_when_none_or_blank() {
|
|
assert_eq!(
|
|
append_reminder_block("SUMMARY".to_string(), None),
|
|
"SUMMARY"
|
|
);
|
|
assert_eq!(
|
|
append_reminder_block("SUMMARY".to_string(), Some(" \n\t ")),
|
|
"SUMMARY"
|
|
);
|
|
}
|
|
}
|