/** * KimiCodeAgentRunner — AgentRunner impl backed by the Kimi Code CLI. * * Architectural difference from ClaudeAgentRunner: * - Claude's `query()` runs the whole agent loop IN-PROCESS (tools execute * inside the same Node process as the daemon). That's why ClaudeAgentRunner * can register org tools (org_send, ask_human, …) directly via * createSdkMcpServer. * - Kimi Code has no embeddable SDK. The CLI is driven as a subprocess: * `kimi -p "" --output-format stream-json` runs one non-interactive * turn and emits JSONL events on stdout (verified against kimi 0.29.2: * {"role":"assistant","content":...} per reply, then a * {"role":"meta","type":"session.resume_hint",session_id} event). * Session continuity comes from `--session ` on later turns. * ARG ORDER MATTERS: the prompt must immediately follow `-p` — flags in * between are consumed as the prompt text. * * Streaming / liveness — WHY INCREMENTAL: * Kimi turns routinely run 10-20+ minutes when the model chains many * internal tool calls (observed: 45+ steps in one turn). session.ts races * the FIRST pull from this runner against a 4-minute silent-stream * watchdog, so buffering stdout until process exit (the original design) * meant any turn longer than 4 minutes yielded zero messages in time — * abort, retry, kill, circuit breaker, stalled org. This runner therefore * parses stdout LINE BY LINE as data arrives: a liveness `tool_use` * message is yielded the moment the subprocess spawns (deterministically * winning the first-pull race regardless of model-thinking latency), * assistant text is yielded as each event lands, and kimi's own * {"role":"tool",...} progress events are forwarded as `tool_use` * liveness messages so the StateDetector/idle watchdog see a working * agent throughout the turn. Tool_call fences are still collected from * the raw texts and parsed at end of turn (fence parsing needs the * complete text). * * Org tools (org_send, knowledge_search, ask_human, …) — FENCE PROTOCOL: * kimi's tool surface can only be extended via MCP servers or plugins, both * loaded by the CLI itself, not by an external caller per-turn. Instead the * tools are rendered INTO the role's system prompt: the model emits * ```tool_call fenced JSON blocks, this runner parses them out of the * assistant text, executes the real OrgToolDef handlers in-process (the same * handlers ClaudeAgentRunner registers with the SDK), and feeds the results * back as the next prompt IN THE SAME kimi session. Loop repeats until a * turn produces no tool calls (cap: MAX_TOOL_ROUNDS). Tool-call fences are * stripped from the text yielded to session.ts so the bus only sees prose. * * Usage accounting — WIRE FILE: * kimi's stream-json has no usage/result event, but every session writes * usage.record entries to $KIMI_CODE_HOME/sessions/// * agents/main/wire.jsonl. After each CLI turn this runner reads the new * entries written since each round started (timestamp-filtered, so a * resumed session's historical entries are never double-counted) and * attaches the summed tokens to the synthesized result message session.ts * needs for budget checks. * * Non-disturbance guarantees (mirrors the opencode integration): * - No new package dependency: the runner shells out to the `kimi` binary * via node:child_process; nothing is imported at module load time. * - The runner is only constructed when MONOMIND_RUNTIME=kimicode is set * (daemon.ts runner resolution). Without the env var, or without a `kimi` * binary on PATH, the Claude path is byte-for-byte unchanged and run() * rejects with a clear actionable error instead of crashing at import. */ import type { AgentMessage, AgentRunArgs, AgentRunner } from './agent-runner.js'; export declare class KimiCodeAgentRunner implements AgentRunner { private kimiBin?; private emptySkillsDir; constructor(kimiBin?: string | undefined); run(args: AgentRunArgs): AsyncIterable; /** * Run one `kimi -p` invocation and stream its stream-json output * INCREMENTALLY: each parsed event is yielded as soon as its line arrives * on stdout (see the header's "Streaming / liveness" note for why buffering * until process exit was a bug). End-of-turn facts (exit code, stderr tail, * final session id, timeout flag) are written into `outcome`, which the * caller reads after this generator completes. */ private streamTurn; /** * Sum usage.record entries in the session's wire.jsonl written at or after * `since` (the round's start time). Timestamp filtering (not line offsets) * is what makes resume safe: a resumed session's wire file already contains * historical entries from previous processes, and those must not be * double-counted. Returns zeros when the wire file can't be found — usage * reporting must never break a turn. */ private readUsageDelta; } /** * One parsed kimi stream-json event, normalized for incremental streaming. * - 'assistant': rawText is the full assistant text (fences intact) for * end-of-turn tool-call parsing; text is the fence-stripped prose, * present only when non-empty. * - 'tool': kimi's own tool activity ({"role":"tool",...}) — forwarded * by run() as a `tool_use` liveness AgentMessage (see header). * - 'meta': any other event that only carries a session id. */ export interface KimiStreamEvent { kind: 'assistant' | 'tool' | 'meta'; text?: string; rawText?: string; toolName?: string; sessionId?: string; } /** * Parse ONE kimi stream-json line into a normalized event (null for blank, * non-JSON, or content-free lines). Exported for unit tests — this encodes * the wire format verified against kimi 0.29.2, and a CLI format change * should fail loudly in CI, not silently starve an org at runtime. * * Real shapes (verified): * {"role":"assistant","content":"..."} — reply text * {"role":"assistant","content":[{"type":"text",...}]} — block form * {"role":"meta","type":"session.resume_hint",session_id} — resume hint * {"role":"tool","content":"Bash(ls ...)"} — tool progress */ export declare function parseStreamJsonLine(line: string): KimiStreamEvent | null; /** * Parse kimi stream-json lines into normalized texts + session id. * Batch convenience wrapper over parseStreamJsonLine, kept for callers/tests * that parse a completed turn's output; the runner itself streams per line. */ export declare function parseStreamJsonLines(lines: string[]): { texts: string[]; rawTexts: string[]; sessionId?: string; }; export interface FatalErrorInfo { fatal: boolean; label?: string; } /** Classify a CLI turn's stderr: is this a fatal (non-retryable) failure? */ export declare function classifyStderr(stderrTail: string): FatalErrorInfo; //# sourceMappingURL=kimicode-runner.d.ts.map