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.
11 KiB
Agent Mode (ACP) and IDE Integration
Agent mode runs Kigi as an ACP (Agent Client Protocol) server for integration with IDEs, editors, and custom tooling. Unlike single-prompt mode (kigi -p, which prints one response and exits), agent mode keeps a persistent process running and communicates through structured JSON-RPC messages.
What is ACP?
The Agent Client Protocol (ACP) is a standard for AI agent communication. It defines how clients (IDEs, editors, custom apps) interact with AI agents through a structured JSON-RPC protocol. ACP provides:
- Session management -- create, load, and resume conversations
- Prompt submission -- send user messages and receive streamed responses
- Tool visibility -- see what tools the agent is using in real time
- Thought streams -- observe the agent's reasoning process
- Permission handling -- approve or deny tool executions interactively
stdio transport
stdio is the primary integration mode. The agent exchanges JSON-RPC messages over stdin and stdout:
kigi agent stdio
Clients that use this mode include:
- IDE extensions (for example, Zed, Neovim, and Emacs)
- Custom automation tools
- ACP client libraries
Options
These options belong to the kigi agent command and apply to every mode. Pass them before the mode name, for example kigi agent --model kigi stdio. The stdio subcommand itself takes no options.
| Flag | Description |
|---|---|
-m, --model <MODEL> |
Set the model ID (for example, kigi). |
--always-approve |
Auto-approve every tool execution. (Alias: --yolo.) |
--reauth |
Run authentication before starting the agent. |
--agent-profile <PATH> |
Load an agent profile from a file. |
Server mode
Run the agent as a WebSocket server for remote clients:
kigi agent serve --bind 127.0.0.1:2419 --secret <token>
Clients connect over WebSocket and authenticate with the secret token. If you omit --secret, the agent generates a token and prints it at startup; you can also supply one through the KIGI_AGENT_SECRET environment variable. The agent persists across reconnections, so a client can disconnect and later resume in-flight work.
WebSocket relay
To reach the agent over the internet instead of the local network, run a WebSocket relay server and have the agent connect to it:
kigi agent headless --kigi-ws-url wss://your-relay.example.com/ws
The agent connects out to your relay, and your web clients connect to the same relay. This is useful for building web UIs where browsers cannot spawn local processes.
ACP protocol basics
Communication follows the JSON-RPC 2.0 format. A typical session lifecycle:
- Initialize -- client sends
initializewith capabilities - Create session -- client sends
session/newwith working directory - Send prompts -- client sends
session/promptwith user messages - Receive updates -- agent sends
session/updatenotifications with streamed content - Handle permissions -- agent may request tool execution approval
Architecture
+------------------------------------------+
| ACP Client |
| (IDE, Editor, Custom Application) |
+-------------------+----------------------+
| JSON-RPC over stdio
+-------------------v----------------------+
| kigi agent stdio |
| |
| +---------+ +---------+ +---------+ |
| | Session | | Tools | | MCP | |
| | Manager | | Registry| | Servers | |
| +---------+ +---------+ +---------+ |
+------------------------------------------+
Streaming updates
ACP streams structured events. Each session/update notification carries a sessionUpdate field that identifies the update type:
sessionUpdate value |
Description |
|---|---|
agent_message_chunk |
A chunk of the agent's response text. |
agent_thought_chunk |
A chunk of the agent's internal reasoning. |
tool_call |
A new tool invocation (title, kind, status, input). |
tool_call_update |
A status or result update for an in-flight tool call. |
plan |
The agent's execution plan. |
Each update names its type, so a client can render distinct panels for reasoning, tool calls, and response text.
Extension methods
Beyond the base ACP protocol, Kigi defines extension methods under the x.ai/ prefix for SpaceXAI-specific functionality. These cover:
| Category | Prefix | Examples |
|---|---|---|
| Filesystem | x.ai/fs/* |
list, exists, read_file, write_file |
| Git | x.ai/git/* |
status, stage, commit, diffs, discard |
| Git Worktree | x.ai/git/worktree/* |
create, remove, apply, list, gc |
| Search | x.ai/search/* |
fuzzy/open, fuzzy/change, content |
| Terminal | x.ai/terminal/* |
create, kill, output, wait_for_exit |
| Session Management | x.ai/session/* |
fork, resolve_local_for_worktree_resume |
| Conversation & History | x.ai/* |
prompt_history, rewind/*, compact_conversation |
| Authentication | x.ai/auth/* |
get_url, submit_code |
| Feedback & Telemetry | x.ai/* |
feedback, telemetry/* |
The tables here show representative methods in each category. The x.ai/* set is SpaceXAI-specific and may expand across releases, so treat it as non-exhaustive and discover the available methods from the agent's initialize response.
Notifications (agent to client)
The agent sends push notifications to clients for real-time updates:
| Notification | Description |
|---|---|
x.ai/search/fuzzy/status |
Fuzzy search results update |
x.ai/git/worktree/status |
Worktree creation progress |
x.ai/fs_notify |
Filesystem change notification |
x.ai/fs/index |
Full file index update |
x.ai/fs/index/delta |
Incremental file index update |
x.ai/session_notification |
Session-specific updates (diff review, retry state, auto-compact) |
x.ai/session/update |
Session update (tool calls, content) |
Session _meta options
The session/new request accepts these optional _meta fields:
| Field | Description |
|---|---|
rules |
Extra rules appended to the system prompt. |
systemPromptOverride |
A replacement system prompt. |
agentProfile |
An agent profile, as a name or a JSON object. |
ACP SDKs
Official SDK libraries are available for multiple languages:
| Language | Package |
|---|---|
| TypeScript | @agentclientprotocol/sdk |
| Rust | agent-client-protocol |
| Python | agent-client-protocol-python |
| Go | acp-go-sdk |
| Kotlin | acp |
Compatible clients
| Client | Status |
|---|---|
| Zed | Supported |
| Neovim (CodeCompanion, avante.nvim) | Supported |
| Emacs | Supported |
| marimo notebook | Supported |
| JetBrains | Coming soon |
Integration example: a TypeScript ACP client
import { spawn, ChildProcess } from "child_process";
import * as readline from "readline";
class KigiACPChat {
private proc!: ChildProcess;
private sessionId!: string;
private rl!: readline.Interface;
constructor(private cwd = ".") {}
async init() {
this.proc = spawn("kigi", ["agent", "stdio"]);
this.rl = readline.createInterface({ input: this.proc.stdout! });
// Initialize
await this.request("initialize", {
protocolVersion: 1,
clientCapabilities: {
fs: { readTextFile: true, writeTextFile: true },
terminal: true,
},
});
// Create session
const { sessionId } = await this.request("session/new", {
cwd: this.cwd,
mcpServers: [],
});
this.sessionId = sessionId;
return this;
}
private async request(method: string, params: any): Promise<any> {
return new Promise((resolve) => {
const msg = JSON.stringify({ jsonrpc: "2.0", id: 1, method, params });
this.proc.stdin!.write(msg + "\n");
this.rl.once("line", (line) => {
resolve(JSON.parse(line).result || {});
});
});
}
async *streamPrompt(text: string) {
const msg = JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "session/prompt",
params: {
sessionId: this.sessionId,
prompt: [{ type: "text", text }],
},
});
this.proc.stdin!.write(msg + "\n");
for await (const line of this.rl) {
const data = JSON.parse(line);
if (data.method === "session/update") {
const update = data.params.update;
yield update; // { sessionUpdate, content, title, ... }
} else if (data.result) {
break; // Final response
}
}
}
}
// Usage
const client = await new KigiACPChat(".").init();
for await (const update of client.streamPrompt("List the files in this project")) {
switch (update.sessionUpdate) {
case "agent_message_chunk":
process.stdout.write(update.content?.text || "");
break;
case "agent_thought_chunk":
console.log(`\n[Thinking: ${update.content?.text}]`);
break;
case "tool_call":
console.log(`\n[Tool: ${update.title}]`);
break;
}
}