@love-moon/ai-sdk 0.2.24 → 0.2.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,878 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { EventEmitter } from "node:events";
3
+ import { KimiWireTransport } from "../transports/kimi-wire-transport.js";
4
+ import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, proxyToEnv, sanitizeForLog, } from "../shared.js";
5
+ const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
6
+ const MIN_TURN_DEADLINE_MS = 30 * 1000;
7
+ const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
8
+ const KIMI_PROVIDER_VARIANT = "kimi-cli-wire";
9
+ const DEFAULT_STATUS_DEDUPE_MS = 120;
10
+ const DEFAULT_STATUS_THROTTLE_MS = 450;
11
+ const DEFAULT_REASONING_STATUS_THROTTLE_MS = 2500;
12
+ const DEFAULT_STATUS_PREVIEW_DELTA_CHARS = 24;
13
+ const MAX_STATUS_TIMING_MS = 10 * 1000;
14
+ function waitForever() {
15
+ return new Promise(() => { });
16
+ }
17
+ function createTurnError(message, extras = {}) {
18
+ const error = new Error(message);
19
+ for (const [key, value] of Object.entries(extras)) {
20
+ error[key] = value;
21
+ }
22
+ return error;
23
+ }
24
+ function normalizeKimiBackend(backend) {
25
+ const normalized = String(backend || "").trim().toLowerCase();
26
+ if (normalized === "kimi-cli" || normalized === "kimi-code") {
27
+ return "kimi";
28
+ }
29
+ return normalized || "kimi";
30
+ }
31
+ function normalizeText(value) {
32
+ return typeof value === "string" ? value : "";
33
+ }
34
+ function sanitizeSummary(value, maxLen = 180) {
35
+ return sanitizeForLog(value, maxLen);
36
+ }
37
+ function normalizeContextUsagePercent(value) {
38
+ const parsed = Number(value);
39
+ if (!Number.isFinite(parsed)) {
40
+ return undefined;
41
+ }
42
+ const bounded = Math.min(Math.max(parsed, 0), 1);
43
+ return Number((bounded * 100).toFixed(1));
44
+ }
45
+ function toolPhaseForName(toolName) {
46
+ const normalized = String(toolName || "").trim().toLowerCase();
47
+ if (!normalized) {
48
+ return "tool_call";
49
+ }
50
+ if (normalized.includes("shell") || normalized.includes("bash") || normalized.includes("command")) {
51
+ return "command_execution";
52
+ }
53
+ if (normalized.includes("write") ||
54
+ normalized.includes("edit") ||
55
+ normalized.includes("replace") ||
56
+ normalized.includes("patch")) {
57
+ return "file_update";
58
+ }
59
+ if (normalized.includes("read") ||
60
+ normalized.includes("grep") ||
61
+ normalized.includes("glob") ||
62
+ normalized.includes("ls")) {
63
+ return "workspace_inspection";
64
+ }
65
+ if (normalized.includes("web") || normalized.includes("search") || normalized.includes("fetch")) {
66
+ return "web_lookup";
67
+ }
68
+ if (normalized.includes("task") || normalized.includes("subagent")) {
69
+ return "task_progress";
70
+ }
71
+ return "tool_call";
72
+ }
73
+ function statusLineForPhase(phase, toolName = "") {
74
+ switch (phase) {
75
+ case "turn_started":
76
+ return "Kimi is working on it";
77
+ case "reasoning":
78
+ return "Kimi is thinking";
79
+ case "planning":
80
+ return "Kimi is planning the next steps";
81
+ case "command_execution":
82
+ return toolName ? `Kimi is running ${toolName}` : "Kimi is running a command";
83
+ case "file_update":
84
+ return toolName ? `Kimi is editing files with ${toolName}` : "Kimi is editing files";
85
+ case "workspace_inspection":
86
+ return toolName ? `Kimi is reading files with ${toolName}` : "Kimi is reading the workspace";
87
+ case "web_lookup":
88
+ return toolName ? `Kimi is browsing with ${toolName}` : "Kimi is browsing the web";
89
+ case "task_progress":
90
+ return toolName ? `Kimi is coordinating ${toolName}` : "Kimi is working on sub-tasks";
91
+ case "context_compaction":
92
+ return "Kimi is compacting context";
93
+ case "message_aggregation":
94
+ return "Kimi is writing the reply";
95
+ case "tool_call":
96
+ return toolName ? `Kimi is calling ${toolName}` : "Kimi is calling a tool";
97
+ default:
98
+ return "Kimi is working";
99
+ }
100
+ }
101
+ function buildEmptyTurnResult() {
102
+ return {
103
+ text: "",
104
+ usage: null,
105
+ items: [],
106
+ events: [],
107
+ };
108
+ }
109
+ export class KimiCliSession extends EventEmitter {
110
+ constructor(backend, options = {}) {
111
+ super();
112
+ this.backend = normalizeKimiBackend(backend);
113
+ this.options = options;
114
+ this.logger = normalizeLogger(options.logger);
115
+ this.cwd =
116
+ typeof options.cwd === "string" && options.cwd.trim()
117
+ ? options.cwd.trim()
118
+ : process.cwd();
119
+ this.resumeSessionId = typeof options.resumeSessionId === "string" ? options.resumeSessionId.trim() : "";
120
+ this.sessionId = this.resumeSessionId || randomUUID();
121
+ this.sessionInfo = {
122
+ backend: this.backend,
123
+ sessionId: this.sessionId,
124
+ };
125
+ this.history = Array.isArray(options.initialHistory) ? [...options.initialHistory] : [];
126
+ this.pendingHistorySeed = this.history.length > 0;
127
+ this.closeRequested = false;
128
+ this.closed = false;
129
+ this.closeWaiters = new Set();
130
+ this.sessionMessageHandler = null;
131
+ this.workingStatusHandler = null;
132
+ this.activeReplyTarget = "";
133
+ this.lastReplyTarget = "";
134
+ this.currentTurn = null;
135
+ this.lastTokenUsage = null;
136
+ this.lastContextUsagePercent = undefined;
137
+ this.turnDeadlineMs = getBoundedEnvInt("CONDUCTOR_TURN_DEADLINE_MS", DEFAULT_TURN_DEADLINE_MS, MIN_TURN_DEADLINE_MS, MAX_TURN_DEADLINE_MS);
138
+ this.workingStatusDedupeMs = getBoundedEnvInt("CONDUCTOR_KIMI_STATUS_DEDUPE_MS", DEFAULT_STATUS_DEDUPE_MS, 0, MAX_STATUS_TIMING_MS);
139
+ this.workingStatusThrottleMs = getBoundedEnvInt("CONDUCTOR_KIMI_STATUS_THROTTLE_MS", DEFAULT_STATUS_THROTTLE_MS, 0, MAX_STATUS_TIMING_MS);
140
+ this.reasoningStatusThrottleMs = getBoundedEnvInt("CONDUCTOR_KIMI_REASONING_STATUS_THROTTLE_MS", DEFAULT_REASONING_STATUS_THROTTLE_MS, 0, MAX_STATUS_TIMING_MS);
141
+ this.workingStatusPreviewDeltaChars = getBoundedEnvInt("CONDUCTOR_KIMI_STATUS_PREVIEW_DELTA_CHARS", DEFAULT_STATUS_PREVIEW_DELTA_CHARS, 1, 500);
142
+ this.now = typeof options.now === "function" ? options.now : () => Date.now();
143
+ this.lastWorkingStatusEmission = null;
144
+ this.bootPromise = null;
145
+ this.booted = false;
146
+ this.sessionAnnounced = false;
147
+ const envConfig = loadEnvConfig(options.configFile);
148
+ const proxyEnv = proxyToEnv(envConfig);
149
+ const extraEnv = envConfig && typeof envConfig === "object" ? { ...envConfig, ...proxyEnv } : proxyEnv;
150
+ this.env = {
151
+ ...extraEnv,
152
+ ...(options.env && typeof options.env === "object" ? options.env : {}),
153
+ };
154
+ this.transport = options.transport || new KimiWireTransport({
155
+ cwd: this.cwd,
156
+ env: this.env,
157
+ logger: {
158
+ log: (message) => {
159
+ this.writeLog(message);
160
+ },
161
+ },
162
+ commandLine: options.commandLine,
163
+ sessionId: this.sessionId,
164
+ model: typeof options.model === "string" ? options.model.trim() : "",
165
+ });
166
+ this.transport.on("event", ({ type, payload }) => {
167
+ void this.handleWireEvent(type, payload);
168
+ });
169
+ this.transport.on("request", ({ id, type, payload }) => {
170
+ void this.handleWireRequest(id, type, payload);
171
+ });
172
+ this.transport.on("process_exit", (payload) => {
173
+ this.handleTransportExit(payload);
174
+ });
175
+ this.transport.on("process_error", (payload) => {
176
+ const error = createTurnError(payload?.message || "Kimi wire transport error", payload || {});
177
+ this.handleTransportFailure(error);
178
+ });
179
+ }
180
+ writeLog(message) {
181
+ emitLog(this.logger, message);
182
+ }
183
+ trace(message) {
184
+ this.writeLog(`[${this.backend}] [kimi-cli] ${message}`);
185
+ }
186
+ get threadId() {
187
+ return this.sessionId;
188
+ }
189
+ get threadOptions() {
190
+ const model = typeof this.options.model === "string" && this.options.model.trim()
191
+ ? this.options.model.trim()
192
+ : this.backend;
193
+ return { model };
194
+ }
195
+ buildManualResumeCommand() {
196
+ if (!this.sessionId) {
197
+ return "";
198
+ }
199
+ if (this.transport && typeof this.transport.buildResumeCommandLine === "function") {
200
+ return this.transport.buildResumeCommandLine();
201
+ }
202
+ return `kimi --work-dir ${this.cwd} --session ${this.sessionId}`;
203
+ }
204
+ getSnapshot() {
205
+ return {
206
+ backend: this.backend,
207
+ provider: KIMI_PROVIDER_VARIANT,
208
+ cwd: this.cwd,
209
+ sessionId: this.sessionId || undefined,
210
+ sessionInfo: this.getSessionInfo(),
211
+ useSessionFileReplyStream: this.usesSessionFileReplyStream(),
212
+ resumeReady: Boolean(this.sessionId),
213
+ manualResume: this.sessionId
214
+ ? {
215
+ ready: true,
216
+ command: this.buildManualResumeCommand(),
217
+ }
218
+ : null,
219
+ pid: this.transport.pid || undefined,
220
+ };
221
+ }
222
+ getSessionInfo() {
223
+ return this.sessionInfo ? { ...this.sessionInfo } : null;
224
+ }
225
+ async ensureSessionInfo() {
226
+ await this.boot();
227
+ return this.getSessionInfo();
228
+ }
229
+ async getSessionUsageSummary() {
230
+ return {
231
+ sessionId: this.sessionId || undefined,
232
+ sessionFilePath: undefined,
233
+ tokenUsagePercent: undefined,
234
+ contextUsagePercent: this.lastContextUsagePercent,
235
+ tokenUsage: this.lastTokenUsage ? { ...this.lastTokenUsage } : null,
236
+ rateLimits: null,
237
+ manualResume: this.sessionId
238
+ ? {
239
+ ready: true,
240
+ command: this.buildManualResumeCommand(),
241
+ }
242
+ : null,
243
+ };
244
+ }
245
+ usesSessionFileReplyStream() {
246
+ return true;
247
+ }
248
+ setSessionMessageHandler(handler) {
249
+ this.sessionMessageHandler = typeof handler === "function" ? handler : null;
250
+ }
251
+ setWorkingStatusHandler(handler) {
252
+ this.workingStatusHandler = typeof handler === "function" ? handler : null;
253
+ }
254
+ setSessionReplyTarget(replyTo) {
255
+ const normalizedReplyTo = typeof replyTo === "string" ? replyTo.trim() : "";
256
+ this.activeReplyTarget = normalizedReplyTo;
257
+ if (normalizedReplyTo) {
258
+ this.lastReplyTarget = normalizedReplyTo;
259
+ }
260
+ }
261
+ getCurrentReplyTarget() {
262
+ return this.activeReplyTarget || this.lastReplyTarget || undefined;
263
+ }
264
+ buildWorkingStatusFingerprint(payload) {
265
+ return JSON.stringify({
266
+ reply_in_progress: Boolean(payload?.reply_in_progress),
267
+ phase: payload?.phase || "",
268
+ status_line: payload?.status_line || "",
269
+ status_done_line: payload?.status_done_line || "",
270
+ reply_preview: payload?.reply_preview || "",
271
+ replyTo: payload?.replyTo || "",
272
+ });
273
+ }
274
+ shouldSuppressWorkingStatus(payload) {
275
+ const previous = this.lastWorkingStatusEmission;
276
+ if (!previous) {
277
+ return false;
278
+ }
279
+ const now = this.now();
280
+ const sameFingerprint = previous.fingerprint === this.buildWorkingStatusFingerprint(payload);
281
+ const elapsedMs = now - previous.emittedAt;
282
+ if (sameFingerprint && elapsedMs >= 0 && elapsedMs < this.workingStatusDedupeMs) {
283
+ return true;
284
+ }
285
+ if (!payload?.reply_in_progress || !previous.payload?.reply_in_progress) {
286
+ return false;
287
+ }
288
+ const samePhaseAndLine = String(previous.payload?.phase || "") === String(payload?.phase || "") &&
289
+ String(previous.payload?.status_line || "") === String(payload?.status_line || "");
290
+ const throttleMs = payload?.phase === "reasoning" || payload?.phase === "planning"
291
+ ? this.reasoningStatusThrottleMs
292
+ : this.workingStatusThrottleMs;
293
+ if (!samePhaseAndLine || elapsedMs < 0 || elapsedMs >= throttleMs) {
294
+ return false;
295
+ }
296
+ if (payload?.status_done_line) {
297
+ return false;
298
+ }
299
+ if (payload?.phase !== "message_aggregation") {
300
+ return true;
301
+ }
302
+ const previousPreview = normalizeText(previous.payload?.reply_preview);
303
+ const nextPreview = normalizeText(payload?.reply_preview);
304
+ if (!nextPreview) {
305
+ return true;
306
+ }
307
+ if (!previousPreview) {
308
+ return false;
309
+ }
310
+ return nextPreview.length < previousPreview.length + this.workingStatusPreviewDeltaChars;
311
+ }
312
+ recordWorkingStatusEmission(payload) {
313
+ this.lastWorkingStatusEmission = {
314
+ fingerprint: this.buildWorkingStatusFingerprint(payload),
315
+ emittedAt: this.now(),
316
+ payload: {
317
+ reply_in_progress: Boolean(payload?.reply_in_progress),
318
+ phase: payload?.phase,
319
+ status_line: payload?.status_line,
320
+ status_done_line: payload?.status_done_line,
321
+ reply_preview: payload?.reply_preview,
322
+ replyTo: payload?.replyTo,
323
+ },
324
+ };
325
+ }
326
+ async emitWorkingStatus(payload, onProgress = null) {
327
+ const normalized = {
328
+ source: KIMI_PROVIDER_VARIANT,
329
+ reply_in_progress: Boolean(payload?.reply_in_progress),
330
+ replyTo: payload?.replyTo || this.getCurrentReplyTarget(),
331
+ state: payload?.state,
332
+ phase: payload?.phase,
333
+ status_line: payload?.status_line,
334
+ status_done_line: payload?.status_done_line,
335
+ reply_preview: payload?.reply_preview,
336
+ thread_id: this.sessionId || undefined,
337
+ session_id: this.sessionId || undefined,
338
+ session_file_path: undefined,
339
+ };
340
+ if (this.shouldSuppressWorkingStatus(normalized)) {
341
+ return;
342
+ }
343
+ this.recordWorkingStatusEmission(normalized);
344
+ if (typeof onProgress === "function") {
345
+ onProgress(normalized);
346
+ }
347
+ if (typeof this.workingStatusHandler === "function") {
348
+ await this.workingStatusHandler(normalized);
349
+ }
350
+ this.emit("working_status", normalized);
351
+ }
352
+ async emitAssistantMessage(text) {
353
+ const payload = {
354
+ text,
355
+ preserveWhitespace: true,
356
+ source: KIMI_PROVIDER_VARIANT,
357
+ replyTo: this.getCurrentReplyTarget(),
358
+ sessionId: this.sessionId || undefined,
359
+ sessionFilePath: undefined,
360
+ timestamp: new Date().toISOString(),
361
+ };
362
+ if (typeof this.sessionMessageHandler === "function") {
363
+ await this.sessionMessageHandler(payload);
364
+ }
365
+ this.emit("assistant_message", payload);
366
+ }
367
+ async emitTerminalWorkingStatus(currentTurn, payload, onProgress = null) {
368
+ if (!currentTurn || currentTurn.terminalWorkingStatusEmitted) {
369
+ return;
370
+ }
371
+ currentTurn.terminalWorkingStatusEmitted = true;
372
+ await this.emitWorkingStatus({
373
+ ...payload,
374
+ reply_in_progress: false,
375
+ }, onProgress);
376
+ }
377
+ createSessionClosedError() {
378
+ const error = new Error("Kimi session closed");
379
+ error.reason = "session_closed";
380
+ return error;
381
+ }
382
+ createTurnTimeoutError(timeoutMs) {
383
+ const seconds = Math.max(1, Math.round(timeoutMs / 1000));
384
+ const error = new Error(`Turn exceeded hard deadline (${seconds}s)`);
385
+ error.reason = "turn_timeout";
386
+ error.timeoutMs = timeoutMs;
387
+ return error;
388
+ }
389
+ createCloseGuard(onClose) {
390
+ if (this.closeRequested) {
391
+ return {
392
+ promise: Promise.reject(this.createSessionClosedError()),
393
+ cleanup: () => { },
394
+ };
395
+ }
396
+ let waiter = null;
397
+ const promise = new Promise((_, reject) => {
398
+ waiter = () => {
399
+ try {
400
+ onClose?.();
401
+ }
402
+ catch {
403
+ // best effort
404
+ }
405
+ reject(this.createSessionClosedError());
406
+ };
407
+ this.closeWaiters.add(waiter);
408
+ });
409
+ return {
410
+ promise,
411
+ cleanup: () => {
412
+ if (waiter) {
413
+ this.closeWaiters.delete(waiter);
414
+ }
415
+ },
416
+ };
417
+ }
418
+ createTurnTimeoutGuard(onTimeout) {
419
+ if (!Number.isFinite(this.turnDeadlineMs) || this.turnDeadlineMs <= 0) {
420
+ return {
421
+ promise: waitForever(),
422
+ cleanup: () => { },
423
+ };
424
+ }
425
+ let timer = null;
426
+ const promise = new Promise((_, reject) => {
427
+ timer = setTimeout(() => {
428
+ try {
429
+ onTimeout?.();
430
+ }
431
+ catch {
432
+ // best effort
433
+ }
434
+ reject(this.createTurnTimeoutError(this.turnDeadlineMs));
435
+ }, this.turnDeadlineMs);
436
+ if (typeof timer.unref === "function") {
437
+ timer.unref();
438
+ }
439
+ });
440
+ return {
441
+ promise,
442
+ cleanup: () => {
443
+ if (timer) {
444
+ clearTimeout(timer);
445
+ }
446
+ },
447
+ };
448
+ }
449
+ flushCloseWaiters() {
450
+ if (this.closeWaiters.size === 0) {
451
+ return;
452
+ }
453
+ for (const waiter of this.closeWaiters) {
454
+ try {
455
+ waiter();
456
+ }
457
+ catch {
458
+ // best effort
459
+ }
460
+ }
461
+ this.closeWaiters.clear();
462
+ }
463
+ buildPrompt(promptText, { useInitialImages = false } = {}) {
464
+ let effectivePrompt = String(promptText || "").trim();
465
+ if (!effectivePrompt) {
466
+ return "";
467
+ }
468
+ if (this.pendingHistorySeed) {
469
+ const historyText = this.history
470
+ .map((item) => {
471
+ const role = String(item?.role || "").toLowerCase() === "assistant" ? "Assistant" : "User";
472
+ return `${role}: ${String(item?.content || "").trim()}`;
473
+ })
474
+ .filter(Boolean)
475
+ .join("\n\n");
476
+ if (historyText) {
477
+ effectivePrompt = [
478
+ "Continue the existing conversation with this history.",
479
+ "",
480
+ historyText,
481
+ "",
482
+ `User: ${effectivePrompt}`,
483
+ ].join("\n");
484
+ }
485
+ this.pendingHistorySeed = false;
486
+ }
487
+ const images = Array.isArray(this.options.initialImages) ? this.options.initialImages : [];
488
+ if (useInitialImages && images.length > 0) {
489
+ const imageContext = images.map((item, idx) => `${idx + 1}. ${item}`).join("\n");
490
+ effectivePrompt = `${effectivePrompt}\n\nAttached image files:\n${imageContext}`;
491
+ }
492
+ return effectivePrompt;
493
+ }
494
+ async boot() {
495
+ if (this.booted) {
496
+ return;
497
+ }
498
+ if (this.bootPromise) {
499
+ return this.bootPromise;
500
+ }
501
+ this.bootPromise = this.bootInternal();
502
+ try {
503
+ await this.bootPromise;
504
+ this.booted = true;
505
+ }
506
+ finally {
507
+ this.bootPromise = null;
508
+ }
509
+ }
510
+ async bootInternal() {
511
+ await this.transport.boot();
512
+ if (!this.sessionAnnounced) {
513
+ this.sessionAnnounced = true;
514
+ this.trace(`session ready id=${this.sessionId}`);
515
+ this.emit("session", this.getSessionInfo());
516
+ }
517
+ }
518
+ updateUsageFromStatus(payload) {
519
+ this.lastContextUsagePercent = normalizeContextUsagePercent(payload?.context_usage);
520
+ this.lastTokenUsage = payload?.token_usage && typeof payload.token_usage === "object"
521
+ ? { ...payload.token_usage }
522
+ : this.lastTokenUsage;
523
+ }
524
+ appendAssistantText(currentTurn, text) {
525
+ const normalized = normalizeText(text);
526
+ if (!normalized) {
527
+ return;
528
+ }
529
+ currentTurn.bufferedAssistantText += normalized;
530
+ }
531
+ async finalizeAssistantMessage(currentTurn) {
532
+ if (!currentTurn) {
533
+ return false;
534
+ }
535
+ const text = currentTurn.bufferedAssistantText;
536
+ if (!text) {
537
+ return false;
538
+ }
539
+ currentTurn.fullText += text;
540
+ currentTurn.bufferedAssistantText = "";
541
+ await this.emitAssistantMessage(text);
542
+ return true;
543
+ }
544
+ maybeEmitAuthRequired(error) {
545
+ const message = String(error?.message || "").toLowerCase();
546
+ if (!message.includes("login") &&
547
+ !message.includes("auth") &&
548
+ !message.includes("credential") &&
549
+ !message.includes("api key") &&
550
+ !message.includes("llm is not set") &&
551
+ !message.includes("not configured")) {
552
+ return;
553
+ }
554
+ this.emit("auth_required", {
555
+ reason: "login_required",
556
+ message: error?.message || "Kimi authentication required",
557
+ });
558
+ }
559
+ async failUnexpectedInteractiveRequest(id, requestType, payload) {
560
+ const currentTurn = this.currentTurn;
561
+ const summary = sanitizeSummary(payload?.description || payload?.action || payload?.name || requestType, 160) || requestType;
562
+ const error = createTurnError(`Kimi CLI requested interactive input in unattended Conductor mode (${requestType})`, {
563
+ reason: "unexpected_interactive_request",
564
+ requestType,
565
+ requestSummary: summary,
566
+ });
567
+ this.handleTransportFailure(error);
568
+ if (currentTurn) {
569
+ await this.emitWorkingStatus({
570
+ phase: "tool_call",
571
+ reply_in_progress: true,
572
+ status_line: summary,
573
+ }, currentTurn.onProgress);
574
+ }
575
+ if (requestType === "ApprovalRequest") {
576
+ this.transport.sendResponse(id, {
577
+ request_id: payload?.id || "",
578
+ response: "reject",
579
+ });
580
+ }
581
+ else if (requestType === "QuestionRequest") {
582
+ this.transport.sendResponse(id, {
583
+ request_id: payload?.id || "",
584
+ answers: {},
585
+ });
586
+ }
587
+ else if (requestType === "ToolCallRequest") {
588
+ this.transport.sendResponse(id, {
589
+ tool_call_id: payload?.id || "",
590
+ return_value: {
591
+ is_error: true,
592
+ output: "",
593
+ message: "External tool calls are not supported in unattended Conductor mode",
594
+ display: [],
595
+ },
596
+ });
597
+ }
598
+ else {
599
+ this.transport.sendError(id, {
600
+ code: -32603,
601
+ message: "Unsupported interactive request",
602
+ });
603
+ }
604
+ void this.interruptCurrentTurn();
605
+ }
606
+ async handleWireRequest(id, type, payload) {
607
+ await this.failUnexpectedInteractiveRequest(id, normalizeText(type), payload && typeof payload === "object" ? payload : {});
608
+ }
609
+ async handleWireEvent(type, payload) {
610
+ const normalizedType = normalizeText(type);
611
+ const normalizedPayload = payload && typeof payload === "object" ? payload : {};
612
+ const currentTurn = this.currentTurn;
613
+ if (currentTurn) {
614
+ currentTurn.items.push({
615
+ type: normalizedType,
616
+ payload: normalizedPayload,
617
+ });
618
+ }
619
+ if (!currentTurn) {
620
+ if (normalizedType === "StatusUpdate") {
621
+ this.updateUsageFromStatus(normalizedPayload);
622
+ }
623
+ return;
624
+ }
625
+ switch (normalizedType) {
626
+ case "TurnBegin":
627
+ await this.emitWorkingStatus({
628
+ phase: "turn_started",
629
+ reply_in_progress: true,
630
+ status_line: statusLineForPhase("turn_started"),
631
+ }, currentTurn.onProgress);
632
+ return;
633
+ case "TurnEnd":
634
+ currentTurn.seenTurnEnd = true;
635
+ return;
636
+ case "StepBegin":
637
+ await this.emitWorkingStatus({
638
+ phase: "reasoning",
639
+ reply_in_progress: true,
640
+ status_line: statusLineForPhase("reasoning"),
641
+ }, currentTurn.onProgress);
642
+ return;
643
+ case "CompactionBegin":
644
+ await this.emitWorkingStatus({
645
+ phase: "context_compaction",
646
+ reply_in_progress: true,
647
+ status_line: statusLineForPhase("context_compaction"),
648
+ }, currentTurn.onProgress);
649
+ return;
650
+ case "StatusUpdate":
651
+ this.updateUsageFromStatus(normalizedPayload);
652
+ if (normalizedPayload.plan_mode === true) {
653
+ await this.emitWorkingStatus({
654
+ phase: "planning",
655
+ reply_in_progress: true,
656
+ status_line: statusLineForPhase("planning"),
657
+ }, currentTurn.onProgress);
658
+ }
659
+ return;
660
+ case "ContentPart":
661
+ if (normalizedPayload.type === "think") {
662
+ await this.emitWorkingStatus({
663
+ phase: "reasoning",
664
+ reply_in_progress: true,
665
+ status_line: statusLineForPhase("reasoning"),
666
+ }, currentTurn.onProgress);
667
+ return;
668
+ }
669
+ if (normalizedPayload.type === "text") {
670
+ this.appendAssistantText(currentTurn, normalizedPayload.text);
671
+ await this.emitWorkingStatus({
672
+ phase: "message_aggregation",
673
+ reply_in_progress: true,
674
+ status_line: statusLineForPhase("message_aggregation"),
675
+ reply_preview: sanitizeSummary(currentTurn.bufferedAssistantText, 120),
676
+ }, currentTurn.onProgress);
677
+ }
678
+ return;
679
+ case "ToolCall": {
680
+ const toolName = normalizeText(normalizedPayload?.function?.name);
681
+ const toolId = normalizeText(normalizedPayload?.id);
682
+ if (toolId) {
683
+ currentTurn.toolCalls.set(toolId, toolName);
684
+ }
685
+ const phase = toolPhaseForName(toolName);
686
+ await this.emitWorkingStatus({
687
+ phase,
688
+ reply_in_progress: true,
689
+ status_line: statusLineForPhase(phase, toolName),
690
+ }, currentTurn.onProgress);
691
+ return;
692
+ }
693
+ case "ToolResult": {
694
+ const toolId = normalizeText(normalizedPayload.tool_call_id);
695
+ const toolName = currentTurn.toolCalls.get(toolId) || "";
696
+ const phase = toolPhaseForName(toolName);
697
+ const statusDoneLine = sanitizeSummary(normalizedPayload?.return_value?.message || normalizedPayload?.return_value?.output, 160) || undefined;
698
+ await this.emitWorkingStatus({
699
+ phase,
700
+ reply_in_progress: true,
701
+ status_line: statusLineForPhase(phase, toolName),
702
+ status_done_line: statusDoneLine,
703
+ }, currentTurn.onProgress);
704
+ return;
705
+ }
706
+ case "SubagentEvent":
707
+ await this.emitWorkingStatus({
708
+ phase: "task_progress",
709
+ reply_in_progress: true,
710
+ status_line: statusLineForPhase("task_progress"),
711
+ }, currentTurn.onProgress);
712
+ return;
713
+ default:
714
+ return;
715
+ }
716
+ }
717
+ handleTransportFailure(error) {
718
+ const currentTurn = this.currentTurn;
719
+ if (!currentTurn || currentTurn.settled) {
720
+ return;
721
+ }
722
+ currentTurn.settled = true;
723
+ currentTurn.reject(error);
724
+ }
725
+ handleTransportExit(payload) {
726
+ const stderrSummary = Array.isArray(payload?.stderr)
727
+ ? sanitizeSummary(payload.stderr.filter(Boolean).at(-1), 200)
728
+ : "";
729
+ const exitError = createTurnError(stderrSummary ? `Kimi CLI exited: ${stderrSummary}` : "Kimi CLI exited", {
730
+ reason: this.closeRequested ? "session_closed" : "transport_exited",
731
+ code: payload?.code,
732
+ signal: payload?.signal,
733
+ stderr: payload?.stderr,
734
+ });
735
+ this.closed = true;
736
+ if (this.closeRequested) {
737
+ this.flushCloseWaiters();
738
+ }
739
+ this.handleTransportFailure(exitError);
740
+ this.emit("process.exited", {
741
+ pid: this.transport.pid || null,
742
+ code: payload?.code,
743
+ signal: payload?.signal,
744
+ stderr: payload?.stderr,
745
+ });
746
+ }
747
+ async interruptCurrentTurn() {
748
+ if (!this.currentTurn) {
749
+ return;
750
+ }
751
+ try {
752
+ await this.transport.request("cancel", {});
753
+ }
754
+ catch {
755
+ // best effort
756
+ }
757
+ }
758
+ async runTurn(promptText, { useInitialImages = false, onProgress = null } = {}) {
759
+ if (this.closeRequested || this.closed) {
760
+ throw this.createSessionClosedError();
761
+ }
762
+ const effectivePrompt = this.buildPrompt(promptText, { useInitialImages });
763
+ if (!effectivePrompt) {
764
+ return buildEmptyTurnResult();
765
+ }
766
+ await this.boot();
767
+ if (this.currentTurn) {
768
+ throw createTurnError("Kimi turn already running", {
769
+ reason: "turn_already_running",
770
+ });
771
+ }
772
+ this.history.push({ role: "user", content: promptText });
773
+ const currentTurn = {
774
+ fullText: "",
775
+ bufferedAssistantText: "",
776
+ items: [],
777
+ toolCalls: new Map(),
778
+ onProgress,
779
+ reject: null,
780
+ settled: false,
781
+ terminalWorkingStatusEmitted: false,
782
+ seenTurnEnd: false,
783
+ };
784
+ const turnFailurePromise = new Promise((_, reject) => {
785
+ currentTurn.reject = reject;
786
+ });
787
+ this.currentTurn = currentTurn;
788
+ const closeGuard = this.createCloseGuard(() => {
789
+ void this.interruptCurrentTurn();
790
+ });
791
+ const turnTimeoutGuard = this.createTurnTimeoutGuard(() => {
792
+ void this.interruptCurrentTurn();
793
+ });
794
+ try {
795
+ await this.emitWorkingStatus({
796
+ phase: "turn_started",
797
+ reply_in_progress: true,
798
+ status_line: statusLineForPhase("turn_started"),
799
+ }, onProgress);
800
+ const promptResult = await Promise.race([
801
+ this.transport.request("prompt", {
802
+ user_input: effectivePrompt,
803
+ }),
804
+ turnFailurePromise,
805
+ closeGuard.promise,
806
+ turnTimeoutGuard.promise,
807
+ ]);
808
+ currentTurn.settled = true;
809
+ await this.finalizeAssistantMessage(currentTurn);
810
+ if (currentTurn.fullText) {
811
+ this.history.push({ role: "assistant", content: currentTurn.fullText });
812
+ }
813
+ const normalizedStatus = normalizeText(promptResult?.status);
814
+ if (normalizedStatus === "cancelled") {
815
+ throw createTurnError("Kimi turn cancelled", {
816
+ reason: "turn_cancelled",
817
+ });
818
+ }
819
+ const statusDoneLine = normalizedStatus === "max_steps_reached"
820
+ ? "Kimi reached the step limit"
821
+ : "Kimi finished";
822
+ await this.emitTerminalWorkingStatus(currentTurn, {
823
+ phase: normalizedStatus === "max_steps_reached" ? "turn_completed" : "turn_completed",
824
+ status_done_line: statusDoneLine,
825
+ }, onProgress);
826
+ this.activeReplyTarget = "";
827
+ return {
828
+ text: currentTurn.fullText,
829
+ usage: this.lastTokenUsage ? { ...this.lastTokenUsage } : null,
830
+ items: currentTurn.items,
831
+ events: currentTurn.items,
832
+ provider: this.backend,
833
+ metadata: {
834
+ source: KIMI_PROVIDER_VARIANT,
835
+ sessionId: this.sessionId || undefined,
836
+ promptStatus: normalizedStatus || undefined,
837
+ contextUsagePercent: this.lastContextUsagePercent,
838
+ },
839
+ };
840
+ }
841
+ catch (error) {
842
+ if (error?.reason === "turn_timeout") {
843
+ await this.interruptCurrentTurn();
844
+ }
845
+ if (!this.closeRequested && error?.reason !== "session_closed") {
846
+ await this.emitTerminalWorkingStatus(currentTurn, {
847
+ phase: "turn_failed",
848
+ status_done_line: String(error?.message || error || "Kimi turn failed"),
849
+ }, onProgress);
850
+ }
851
+ if (this.closeRequested && error?.reason !== "session_closed") {
852
+ throw this.createSessionClosedError();
853
+ }
854
+ this.maybeEmitAuthRequired(error);
855
+ throw error;
856
+ }
857
+ finally {
858
+ this.activeReplyTarget = "";
859
+ if (this.currentTurn === currentTurn) {
860
+ this.currentTurn = null;
861
+ }
862
+ closeGuard.cleanup();
863
+ turnTimeoutGuard.cleanup();
864
+ }
865
+ }
866
+ async close() {
867
+ if (this.closed) {
868
+ return;
869
+ }
870
+ this.closeRequested = true;
871
+ this.flushCloseWaiters();
872
+ if (this.currentTurn) {
873
+ await this.interruptCurrentTurn();
874
+ }
875
+ await this.transport.close();
876
+ this.closed = true;
877
+ }
878
+ }