@love-moon/ai-sdk 0.13.0 → 0.13.2

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.
@@ -6,11 +6,17 @@ import os from "node:os";
6
6
  import path from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { DSH_SDK_VARIANT } from "../built-in-backends.js";
9
- import { emitLog, extractLongFlagFromCommandLine, getBoundedEnvInt, loadEnvConfig, normalizeLogger, proxyToEnv, sanitizeForLog, } from "../shared.js";
9
+ import { emitLog, extractLongFlagFromCommandLine, getBoundedEnvInt, loadEnvConfig, normalizeLogger, noteToolFinished, noteToolStarted, proxyToEnv, sanitizeForLog, withActiveTool, } from "../shared.js";
10
10
  const require = createRequire(import.meta.url);
11
11
  const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
12
12
  const MIN_TURN_DEADLINE_MS = 30 * 1000;
13
13
  const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
14
+ export const COMPACT_SUMMARY_PROMPT = [
15
+ "Summarize our conversation so far so it can replace the full history.",
16
+ "Keep the goals, decisions, current state of the work, relevant files and commands, and open next steps.",
17
+ "Do not use tools. Reply with the summary only.",
18
+ ].join(" ");
19
+ const COMPACT_SUMMARY_PREFIX = "Summary of the conversation so far:\n\n";
14
20
  // Pinned defaults for the dsh runtime route. `deepseek-official` is the
15
21
  // provider route the stock `@deepseek-ai/dsh-llm-deepseek` adapter registers;
16
22
  // the model can be overridden via options.model or an allow_cli_list command
@@ -126,7 +132,7 @@ function accumulateUsage(total, usage) {
126
132
  * the next runTurn spawns a fresh runtime resuming the same session id.
127
133
  */
128
134
  export class DshSdkSession extends EventEmitter {
129
- static capabilities = Object.freeze({ goal: false });
135
+ static capabilities = Object.freeze({ goal: false, compact: true });
130
136
  getCapabilities() {
131
137
  return { ...DshSdkSession.capabilities };
132
138
  }
@@ -220,7 +226,7 @@ export class DshSdkSession extends EventEmitter {
220
226
  return this.sessionInfo ? { ...this.sessionInfo } : null;
221
227
  }
222
228
  getCurrentTurnStatus() {
223
- return this.currentTurnStatus ? { ...this.currentTurnStatus } : null;
229
+ return withActiveTool(this.currentTurnStatus, this.currentTurn);
224
230
  }
225
231
  async ensureSessionInfo() {
226
232
  return this.getSessionInfo();
@@ -376,6 +382,7 @@ export class DshSdkSession extends EventEmitter {
376
382
  return [];
377
383
  }
378
384
  const restored = [];
385
+ let compactSummaryPending = false;
379
386
  let content;
380
387
  try {
381
388
  content = fs.readFileSync(logPath, "utf8");
@@ -397,14 +404,26 @@ export class DshSdkSession extends EventEmitter {
397
404
  }
398
405
  if (event?.type === "user/message" && event.data?.source?.kind === "user") {
399
406
  const text = extractTextBlocks(event.data?.content);
400
- if (text) {
407
+ // A `/compact` summary turn supersedes everything before it, but only
408
+ // once its reply lands; a failed or interrupted one changes nothing.
409
+ compactSummaryPending = text.includes(COMPACT_SUMMARY_PROMPT);
410
+ if (text && !compactSummaryPending) {
401
411
  restored.push({ role: "user", content: text });
402
412
  }
403
413
  continue;
404
414
  }
415
+ if (event?.type === "turn/end" && event.data?.reason?.kind !== "completed") {
416
+ compactSummaryPending = false;
417
+ continue;
418
+ }
405
419
  if (event?.type === "assistant/message") {
406
420
  const text = extractTextBlocks(event.data?.message?.content);
407
- if (text) {
421
+ if (text && compactSummaryPending) {
422
+ restored.length = 0;
423
+ restored.push({ role: "assistant", content: `${COMPACT_SUMMARY_PREFIX}${text}` });
424
+ compactSummaryPending = false;
425
+ }
426
+ else if (text) {
408
427
  restored.push({ role: "assistant", content: text });
409
428
  }
410
429
  }
@@ -701,8 +720,11 @@ export class DshSdkSession extends EventEmitter {
701
720
  if (!text) {
702
721
  return;
703
722
  }
704
- currentTurn.emittedAssistantMessage = true;
705
723
  currentTurn.fullText = text;
724
+ if (currentTurn.suppressReply) {
725
+ return;
726
+ }
727
+ currentTurn.emittedAssistantMessage = true;
706
728
  await this.emitWorkingStatus({
707
729
  phase: "message_aggregation",
708
730
  reply_in_progress: true,
@@ -714,6 +736,7 @@ export class DshSdkSession extends EventEmitter {
714
736
  }
715
737
  case "tool/call": {
716
738
  const toolName = String(event.data?.name || "");
739
+ noteToolStarted(currentTurn, event.data?.callId, toolName, event.data?.arguments);
717
740
  const phase = toolPhaseForName(toolName);
718
741
  const argsPreview = sanitizeForLog(event.data?.arguments, 120);
719
742
  await this.emitWorkingStatus({
@@ -723,6 +746,9 @@ export class DshSdkSession extends EventEmitter {
723
746
  }, onProgress);
724
747
  return;
725
748
  }
749
+ case "tool/result":
750
+ noteToolFinished(currentTurn, event.data?.message?.content?.[0]?.toolCallId);
751
+ return;
726
752
  case "todo/write": {
727
753
  const todos = Array.isArray(event.data?.todos) ? event.data.todos : [];
728
754
  const done = todos.filter((item) => item?.status === "completed").length;
@@ -741,6 +767,15 @@ export class DshSdkSession extends EventEmitter {
741
767
  }, onProgress);
742
768
  return;
743
769
  }
770
+ case "compaction/end": {
771
+ // Automatic compaction finished mid-turn; drop the "compacting" line.
772
+ await this.emitWorkingStatus({
773
+ phase: "turn_started",
774
+ reply_in_progress: true,
775
+ status_line: "dsh is working",
776
+ }, onProgress);
777
+ return;
778
+ }
744
779
  case "turn/end": {
745
780
  currentTurn.turnEndReason = event.data?.reason || null;
746
781
  return;
@@ -762,7 +797,40 @@ export class DshSdkSession extends EventEmitter {
762
797
  await this.disposeHarness();
763
798
  return true;
764
799
  }
765
- async runTurn(promptText, { useInitialImages = false, onProgress = null } = {}) {
800
+ /**
801
+ * Manually compact the conversation. The dsh SDK wire exposes no compaction
802
+ * request (its engine's `compactNow` is internal), so the model summarizes
803
+ * the conversation in one silent turn and the chat continues on a fresh
804
+ * wire session seeded with only that summary.
805
+ *
806
+ * @param {import("../shared.js").CompactRequest} [request]
807
+ * @param {{ onProgress?: Function }} [options]
808
+ * @returns {Promise<import("../shared.js").CompactResult>}
809
+ */
810
+ async runCompact(request = {}, { onProgress = null } = {}) {
811
+ const instructions = typeof request?.instructions === "string" ? request.instructions.trim() : "";
812
+ const resumableLog = this.pendingResumeFromSessionId && this.findPersistedSessionLog(this.pendingResumeFromSessionId);
813
+ if (this.history.length === 0 && !resumableLog) {
814
+ return { compact: { status: "noop", instructionsApplied: false }, usage: null, metadata: {} };
815
+ }
816
+ const prompt = instructions ? `${COMPACT_SUMMARY_PROMPT}\n\nAdditional focus: ${instructions}` : COMPACT_SUMMARY_PROMPT;
817
+ const turnResult = await this.runTurn(prompt, { onProgress, suppressReply: true });
818
+ const summary = String(turnResult.text || "").trim();
819
+ if (!summary) {
820
+ throw createTurnError("dsh compaction produced an empty summary", { reason: "compact_failed" });
821
+ }
822
+ this.history = [{ role: "assistant", content: `${COMPACT_SUMMARY_PREFIX}${summary}` }];
823
+ // Dropping the runtime makes the next turn rotate onto a fresh id seeded
824
+ // with the summary. Rotating here instead would expose the new id for
825
+ // binding before it has a persisted log, so a restart would resume empty.
826
+ await this.disposeHarness();
827
+ return {
828
+ compact: { status: "compacted", instructionsApplied: Boolean(instructions) },
829
+ usage: turnResult.usage,
830
+ metadata: { source: DSH_SDK_VARIANT, sessionId: this.sessionId },
831
+ };
832
+ }
833
+ async runTurn(promptText, { useInitialImages = false, onProgress = null, suppressReply = false } = {}) {
766
834
  if (this.closeRequested) {
767
835
  throw this.createSessionClosedError();
768
836
  }
@@ -794,8 +862,11 @@ export class DshSdkSession extends EventEmitter {
794
862
  this.rotateSessionId();
795
863
  }
796
864
  const effectivePrompt = this.buildPrompt(promptText, { useInitialImages });
797
- this.history.push({ role: "user", content: promptText });
865
+ if (!suppressReply) {
866
+ this.history.push({ role: "user", content: promptText });
867
+ }
798
868
  const currentTurn = {
869
+ suppressReply,
799
870
  emittedAssistantMessage: false,
800
871
  fullText: "",
801
872
  interrupted: false,
@@ -857,10 +928,10 @@ export class DshSdkSession extends EventEmitter {
857
928
  const responseText = typeof runResult?.finalResponse === "string" && runResult.finalResponse
858
929
  ? runResult.finalResponse
859
930
  : currentTurn.fullText;
860
- if (!currentTurn.emittedAssistantMessage && responseText) {
931
+ if (!suppressReply && !currentTurn.emittedAssistantMessage && responseText) {
861
932
  await this.emitAssistantMessage(responseText);
862
933
  }
863
- if (responseText) {
934
+ if (!suppressReply && responseText) {
864
935
  this.history.push({ role: "assistant", content: responseText });
865
936
  }
866
937
  const turnEndKind = currentTurn.turnEndReason?.kind;
@@ -20,6 +20,8 @@ export class KimiCliSession extends EventEmitter<[never]> {
20
20
  activeReplyTarget: string;
21
21
  lastReplyTarget: string;
22
22
  currentTurn: {
23
+ suppressReply: boolean;
24
+ compacted: boolean;
23
25
  fullText: string;
24
26
  bufferedAssistantText: string;
25
27
  thinkText: string;
@@ -84,6 +86,7 @@ export class KimiCliSession extends EventEmitter<[never]> {
84
86
  } | null;
85
87
  currentTurnStatus: any;
86
88
  capabilities: {
89
+ compact: boolean;
87
90
  media: Readonly<{
88
91
  image: "native";
89
92
  }>;
@@ -154,10 +157,23 @@ export class KimiCliSession extends EventEmitter<[never]> {
154
157
  handleTransportFailure(error: any): void;
155
158
  handleTransportExit(payload: any): void;
156
159
  interruptCurrentTurn(): Promise<boolean>;
157
- runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, onProgress, jsonSchema }?: {
160
+ /**
161
+ * Manually compact via Kimi CLI's built-in `/compact` slash command, which
162
+ * the wire `prompt` method dispatches like the interactive shell does. The
163
+ * command takes no focus instructions.
164
+ *
165
+ * @param {import("../shared.js").CompactRequest} [request]
166
+ * @param {{ onProgress?: Function }} [options]
167
+ * @returns {Promise<import("../shared.js").CompactResult>}
168
+ */
169
+ runCompact(request?: import("../shared.js").CompactRequest, { onProgress }?: {
170
+ onProgress?: Function;
171
+ }): Promise<import("../shared.js").CompactResult>;
172
+ runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, onProgress, jsonSchema, suppressReply }?: {
158
173
  useInitialImages?: boolean | undefined;
159
174
  onProgress?: null | undefined;
160
175
  jsonSchema?: null | undefined;
176
+ suppressReply?: boolean | undefined;
161
177
  }): Promise<{
162
178
  text: string;
163
179
  usage: null;
@@ -175,6 +191,7 @@ export class KimiCliSession extends EventEmitter<[never]> {
175
191
  promptStatus: string | undefined;
176
192
  contextUsagePercent: number | undefined;
177
193
  };
194
+ compacted: boolean;
178
195
  }>;
179
196
  close(): Promise<void>;
180
197
  }
@@ -5,7 +5,7 @@ import { appendContextFilesToPrompt } from "../context-files.js";
5
5
  import { PROVIDER_MEDIA_CAPABILITIES, buildKimiContent } from "../media-adapters.js";
6
6
  import { assertMediaCapabilities, defaultPromptForMedia, resolveTurnMedia, } from "../media-input.js";
7
7
  import { KimiWireTransport } from "../transports/kimi-wire-transport.js";
8
- import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, proxyToEnv, sanitizeForLog, } from "../shared.js";
8
+ import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, noteToolFinished, noteToolStarted, proxyToEnv, sanitizeForLog, withActiveTool, } from "../shared.js";
9
9
  const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
10
10
  const MIN_TURN_DEADLINE_MS = 30 * 1000;
11
11
  const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
@@ -233,7 +233,7 @@ export class KimiCliSession extends EventEmitter {
233
233
  }
234
234
  : null,
235
235
  currentTurnStatus: this.getCurrentTurnStatus(),
236
- capabilities: { media: PROVIDER_MEDIA_CAPABILITIES[KIMI_PROVIDER_VARIANT] },
236
+ capabilities: { compact: true, media: PROVIDER_MEDIA_CAPABILITIES[KIMI_PROVIDER_VARIANT] },
237
237
  pid: this.transport.pid || undefined,
238
238
  };
239
239
  }
@@ -241,7 +241,7 @@ export class KimiCliSession extends EventEmitter {
241
241
  return this.sessionInfo ? { ...this.sessionInfo } : null;
242
242
  }
243
243
  getCurrentTurnStatus() {
244
- return this.currentTurnStatus ? { ...this.currentTurnStatus } : null;
244
+ return withActiveTool(this.currentTurnStatus, this.currentTurn);
245
245
  }
246
246
  async ensureSessionInfo() {
247
247
  await this.boot();
@@ -642,6 +642,9 @@ export class KimiCliSession extends EventEmitter {
642
642
  }
643
643
  currentTurn.fullText += text;
644
644
  currentTurn.bufferedAssistantText = "";
645
+ if (currentTurn.suppressReply) {
646
+ return false;
647
+ }
645
648
  await this.emitAssistantMessage(text);
646
649
  return true;
647
650
  }
@@ -760,6 +763,7 @@ export class KimiCliSession extends EventEmitter {
760
763
  }, currentTurn.onProgress);
761
764
  return;
762
765
  case "CompactionBegin":
766
+ currentTurn.compacted = true;
763
767
  await this.emitWorkingStatus({
764
768
  phase: "context_compaction",
765
769
  reply_in_progress: true,
@@ -792,7 +796,9 @@ export class KimiCliSession extends EventEmitter {
792
796
  phase: "message_aggregation",
793
797
  reply_in_progress: true,
794
798
  status_line: statusLineForPhase("message_aggregation"),
795
- reply_preview: sanitizeSummary(currentTurn.bufferedAssistantText, 120),
799
+ reply_preview: currentTurn.suppressReply
800
+ ? undefined
801
+ : sanitizeSummary(currentTurn.bufferedAssistantText, 120),
796
802
  }, currentTurn.onProgress);
797
803
  }
798
804
  return;
@@ -802,6 +808,7 @@ export class KimiCliSession extends EventEmitter {
802
808
  if (toolId) {
803
809
  currentTurn.toolCalls.set(toolId, toolName);
804
810
  }
811
+ noteToolStarted(currentTurn, toolId, toolName, normalizedPayload?.function?.arguments);
805
812
  const phase = toolPhaseForName(toolName);
806
813
  await this.emitWorkingStatus({
807
814
  phase,
@@ -813,6 +820,7 @@ export class KimiCliSession extends EventEmitter {
813
820
  case "ToolResult": {
814
821
  const toolId = normalizeText(normalizedPayload.tool_call_id);
815
822
  const toolName = currentTurn.toolCalls.get(toolId) || "";
823
+ noteToolFinished(currentTurn, toolId);
816
824
  const phase = toolPhaseForName(toolName);
817
825
  const statusDoneLine = sanitizeSummary(normalizedPayload?.return_value?.message || normalizedPayload?.return_value?.output, 160) || undefined;
818
826
  await this.emitWorkingStatus({
@@ -877,7 +885,27 @@ export class KimiCliSession extends EventEmitter {
877
885
  }
878
886
  return false;
879
887
  }
880
- async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null, jsonSchema = null } = {}) {
888
+ /**
889
+ * Manually compact via Kimi CLI's built-in `/compact` slash command, which
890
+ * the wire `prompt` method dispatches like the interactive shell does. The
891
+ * command takes no focus instructions.
892
+ *
893
+ * @param {import("../shared.js").CompactRequest} [request]
894
+ * @param {{ onProgress?: Function }} [options]
895
+ * @returns {Promise<import("../shared.js").CompactResult>}
896
+ */
897
+ async runCompact(request = {}, { onProgress = null } = {}) {
898
+ if (this.pendingHistorySeed) {
899
+ return { compact: { status: "noop", instructionsApplied: false }, usage: null, metadata: {} };
900
+ }
901
+ const turnResult = await this.runTurn("/compact", { onProgress, suppressReply: true });
902
+ return {
903
+ compact: { status: turnResult.compacted ? "compacted" : "noop", instructionsApplied: false },
904
+ usage: turnResult.usage,
905
+ metadata: turnResult.metadata,
906
+ };
907
+ }
908
+ async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null, jsonSchema = null, suppressReply = false } = {}) {
881
909
  if (this.closeRequested || this.closed) {
882
910
  throw this.createSessionClosedError();
883
911
  }
@@ -905,8 +933,12 @@ export class KimiCliSession extends EventEmitter {
905
933
  await this.failPendingTurnStart(error, onProgress);
906
934
  throw error;
907
935
  }
908
- this.history.push({ role: "user", content: promptText });
936
+ if (!suppressReply) {
937
+ this.history.push({ role: "user", content: promptText });
938
+ }
909
939
  const currentTurn = {
940
+ suppressReply,
941
+ compacted: false,
910
942
  fullText: "",
911
943
  bufferedAssistantText: "",
912
944
  thinkText: "",
@@ -948,7 +980,7 @@ export class KimiCliSession extends EventEmitter {
948
980
  ]);
949
981
  currentTurn.settled = true;
950
982
  await this.finalizeAssistantMessage(currentTurn);
951
- if (currentTurn.fullText) {
983
+ if (currentTurn.fullText && !suppressReply) {
952
984
  this.history.push({ role: "assistant", content: currentTurn.fullText });
953
985
  }
954
986
  const normalizedStatus = normalizeText(promptResult?.status);
@@ -977,6 +1009,7 @@ export class KimiCliSession extends EventEmitter {
977
1009
  promptStatus: normalizedStatus || undefined,
978
1010
  contextUsagePercent: this.lastContextUsagePercent,
979
1011
  },
1012
+ compacted: currentTurn.compacted,
980
1013
  };
981
1014
  }
982
1015
  catch (error) {
@@ -61,6 +61,7 @@ export class KimiPrintSession extends EventEmitter<[never]> {
61
61
  } | null;
62
62
  currentTurnStatus: any;
63
63
  capabilities: {
64
+ compact: boolean;
64
65
  media: Readonly<{
65
66
  image: "unsupported";
66
67
  }> | Readonly<{
@@ -115,10 +116,24 @@ export class KimiPrintSession extends EventEmitter<[never]> {
115
116
  };
116
117
  maybeEmitAuthRequired(stderrTail: any): void;
117
118
  interruptCurrentTurn(): Promise<boolean>;
118
- runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, onProgress, jsonSchema }?: {
119
+ /**
120
+ * Manually compact via kimi-cli's built-in `/compact` slash command; print
121
+ * mode runs it through the same soul as the interactive shell. Print output
122
+ * carries no compaction events, so the outcome is read from the CLI's fixed
123
+ * replies. The command takes no focus instructions.
124
+ *
125
+ * @param {import("../shared.js").CompactRequest} [request]
126
+ * @param {{ onProgress?: Function }} [options]
127
+ * @returns {Promise<import("../shared.js").CompactResult>}
128
+ */
129
+ runCompact(request?: import("../shared.js").CompactRequest, { onProgress }?: {
130
+ onProgress?: Function;
131
+ }): Promise<import("../shared.js").CompactResult>;
132
+ runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, onProgress, jsonSchema, suppressReply }?: {
119
133
  useInitialImages?: boolean | undefined;
120
134
  onProgress?: null | undefined;
121
135
  jsonSchema?: null | undefined;
136
+ suppressReply?: boolean | undefined;
122
137
  }): Promise<{
123
138
  text: string;
124
139
  usage: null;
@@ -8,7 +8,7 @@ import { KIMI_CLI_PRINT_VARIANT as KIMI_PRINT_PROVIDER_VARIANT } from "../built-
8
8
  import { appendContextFilesToPrompt } from "../context-files.js";
9
9
  import { PROVIDER_MEDIA_CAPABILITIES, UNSUPPORTED_MEDIA_CAPABILITIES, buildKimiContent, } from "../media-adapters.js";
10
10
  import { assertMediaCapabilities, defaultPromptForMedia, resolveTurnMedia, } from "../media-input.js";
11
- import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, parseCommandParts, proxyToEnv, sanitizeForLog, } from "../shared.js";
11
+ import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, noteToolFinished, noteToolStarted, parseCommandParts, proxyToEnv, sanitizeForLog, withActiveTool, } from "../shared.js";
12
12
  const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
13
13
  const MIN_TURN_DEADLINE_MS = 30 * 1000;
14
14
  const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
@@ -270,6 +270,8 @@ export class KimiPrintSession extends EventEmitter {
270
270
  : null,
271
271
  currentTurnStatus: this.getCurrentTurnStatus(),
272
272
  capabilities: {
273
+ // Kimi Code's `--prompt` CLI is unverified for slash commands.
274
+ compact: this.cliMode !== "prompt",
273
275
  media: this.cliMode === "prompt"
274
276
  ? UNSUPPORTED_MEDIA_CAPABILITIES
275
277
  : PROVIDER_MEDIA_CAPABILITIES[KIMI_PRINT_PROVIDER_VARIANT],
@@ -281,7 +283,7 @@ export class KimiPrintSession extends EventEmitter {
281
283
  return this.sessionInfo ? { ...this.sessionInfo } : null;
282
284
  }
283
285
  getCurrentTurnStatus() {
284
- return this.currentTurnStatus ? { ...this.currentTurnStatus } : null;
286
+ return withActiveTool(this.currentTurnStatus, this.currentTurn);
285
287
  }
286
288
  async ensureSessionInfo() {
287
289
  if (!this.sessionInfo?.sessionIdDeferred) {
@@ -468,7 +470,42 @@ export class KimiPrintSession extends EventEmitter {
468
470
  return false;
469
471
  }
470
472
  }
471
- async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null, jsonSchema = null } = {}) {
473
+ /**
474
+ * Manually compact via kimi-cli's built-in `/compact` slash command; print
475
+ * mode runs it through the same soul as the interactive shell. Print output
476
+ * carries no compaction events, so the outcome is read from the CLI's fixed
477
+ * replies. The command takes no focus instructions.
478
+ *
479
+ * @param {import("../shared.js").CompactRequest} [request]
480
+ * @param {{ onProgress?: Function }} [options]
481
+ * @returns {Promise<import("../shared.js").CompactResult>}
482
+ */
483
+ async runCompact(request = {}, { onProgress = null } = {}) {
484
+ if (this.cliMode === "prompt") {
485
+ throw createTurnError("Kimi prompt mode does not support /compact", { reason: "unsupported_compact" });
486
+ }
487
+ if (this.pendingHistorySeed) {
488
+ return { compact: { status: "noop", instructionsApplied: false }, usage: null, metadata: {} };
489
+ }
490
+ const turnResult = await this.runTurn("/compact", { onProgress, suppressReply: true });
491
+ const reply = String(turnResult.text || "");
492
+ // Print mode exits 0 even when the model call fails, so only kimi-cli's
493
+ // fixed replies are trusted.
494
+ if (!/context has been compacted|context is empty/i.test(reply)) {
495
+ throw createTurnError(`Kimi compaction failed${reply.trim() ? `: ${reply.trim()}` : ""}`, {
496
+ reason: "compact_failed",
497
+ });
498
+ }
499
+ return {
500
+ compact: {
501
+ status: /context is empty/i.test(reply) ? "noop" : "compacted",
502
+ instructionsApplied: false,
503
+ },
504
+ usage: turnResult.usage,
505
+ metadata: turnResult.metadata,
506
+ };
507
+ }
508
+ async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null, jsonSchema = null, suppressReply = false } = {}) {
472
509
  if (this.closeRequested || this.closed) {
473
510
  throw this.createSessionClosedError();
474
511
  }
@@ -497,7 +534,9 @@ export class KimiPrintSession extends EventEmitter {
497
534
  return buildEmptyTurnResult();
498
535
  }
499
536
  this.announceSession();
500
- this.history.push({ role: "user", content: String(promptText || "") });
537
+ if (!suppressReply) {
538
+ this.history.push({ role: "user", content: String(promptText || "") });
539
+ }
501
540
  const currentTurn = {
502
541
  child: null,
503
542
  fullText: "",
@@ -606,6 +645,9 @@ export class KimiPrintSession extends EventEmitter {
606
645
  if (role === "assistant") {
607
646
  const assistantText = normalizeTextContent(payload.content);
608
647
  if (Array.isArray(payload?.tool_calls) && payload.tool_calls.length > 0) {
648
+ for (const call of payload.tool_calls) {
649
+ noteToolStarted(currentTurn, call?.id, call?.function?.name, call?.function?.arguments);
650
+ }
609
651
  void this.emitWorkingStatus({
610
652
  phase: "command_execution",
611
653
  reply_in_progress: true,
@@ -621,11 +663,14 @@ export class KimiPrintSession extends EventEmitter {
621
663
  }
622
664
  if (assistantText) {
623
665
  currentTurn.fullText += assistantText;
624
- void this.emitAssistantMessage(assistantText);
666
+ if (!suppressReply) {
667
+ void this.emitAssistantMessage(assistantText);
668
+ }
625
669
  }
626
670
  return;
627
671
  }
628
672
  if (role === "tool") {
673
+ noteToolFinished(currentTurn, payload.tool_call_id);
629
674
  void this.emitWorkingStatus({
630
675
  phase: "command_execution",
631
676
  reply_in_progress: true,
@@ -684,7 +729,7 @@ export class KimiPrintSession extends EventEmitter {
684
729
  }
685
730
  child.stdin.end();
686
731
  });
687
- if (result?.text) {
732
+ if (result?.text && !suppressReply) {
688
733
  this.history.push({ role: "assistant", content: result.text });
689
734
  }
690
735
  this.activeReplyTarget = "";
@@ -20,7 +20,7 @@ export class OpencodeSdkSession extends EventEmitter<[never]> {
20
20
  activeReplyTarget: string;
21
21
  lastReplyTarget: string;
22
22
  currentTurn: {
23
- abortController: AbortController;
23
+ abortController: any;
24
24
  assistantMessages: Map<any, any>;
25
25
  assistantMessageOrder: never[];
26
26
  activeAssistantMessageId: string;
@@ -29,7 +29,7 @@ export class OpencodeSdkSession extends EventEmitter<[never]> {
29
29
  fullText: string;
30
30
  items: never[];
31
31
  lastAssistantInfo: null;
32
- onProgress: null;
32
+ onProgress: any;
33
33
  resolve: null;
34
34
  reject: null;
35
35
  settled: boolean;
@@ -79,6 +79,7 @@ export class OpencodeSdkSession extends EventEmitter<[never]> {
79
79
  manualResume: null;
80
80
  currentTurnStatus: any;
81
81
  capabilities: {
82
+ compact: boolean;
82
83
  media: Readonly<{
83
84
  image: "native";
84
85
  }>;
@@ -169,6 +170,47 @@ export class OpencodeSdkSession extends EventEmitter<[never]> {
169
170
  handleTransportFailure(error: any): void;
170
171
  handleTransportExit(payload: any): void;
171
172
  interruptCurrentTurn(): Promise<boolean>;
173
+ createTurnState(abortController: any, onProgress: any): {
174
+ currentTurn: {
175
+ abortController: any;
176
+ assistantMessages: Map<any, any>;
177
+ assistantMessageOrder: never[];
178
+ activeAssistantMessageId: string;
179
+ messageRoles: Map<any, any>;
180
+ pendingMessageEvents: Map<any, any>;
181
+ fullText: string;
182
+ items: never[];
183
+ lastAssistantInfo: null;
184
+ onProgress: any;
185
+ resolve: null;
186
+ reject: null;
187
+ settled: boolean;
188
+ terminalWorkingStatusEmitted: boolean;
189
+ };
190
+ completionPromise: Promise<any>;
191
+ };
192
+ /**
193
+ * Model to summarize with: `session.summarize` requires one explicitly.
194
+ * Prefer what the session actually ran on (last turn, then its recent
195
+ * messages); the configured model is only a fallback.
196
+ */
197
+ resolveCompactionModel(): Promise<{
198
+ providerID: any;
199
+ modelID: any;
200
+ }>;
201
+ /**
202
+ * Manually compact via the server's `session.summarize` (what the TUI's
203
+ * `/compact` calls). It takes no focus instructions and answers `true`
204
+ * even when summarizing fails, so success is judged from the summary
205
+ * message itself.
206
+ *
207
+ * @param {import("../shared.js").CompactRequest} [request]
208
+ * @param {{ onProgress?: Function }} [options]
209
+ * @returns {Promise<import("../shared.js").CompactResult>}
210
+ */
211
+ runCompact(request?: import("../shared.js").CompactRequest, { onProgress }?: {
212
+ onProgress?: Function;
213
+ }): Promise<import("../shared.js").CompactResult>;
172
214
  runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, onProgress, jsonSchema }?: {
173
215
  useInitialImages?: boolean | undefined;
174
216
  onProgress?: null | undefined;