@love-moon/ai-sdk 0.13.1 → 0.13.3

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.
@@ -11,6 +11,12 @@ 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
  }
@@ -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,
@@ -745,6 +767,15 @@ export class DshSdkSession extends EventEmitter {
745
767
  }, onProgress);
746
768
  return;
747
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
+ }
748
779
  case "turn/end": {
749
780
  currentTurn.turnEndReason = event.data?.reason || null;
750
781
  return;
@@ -766,7 +797,40 @@ export class DshSdkSession extends EventEmitter {
766
797
  await this.disposeHarness();
767
798
  return true;
768
799
  }
769
- 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 } = {}) {
770
834
  if (this.closeRequested) {
771
835
  throw this.createSessionClosedError();
772
836
  }
@@ -798,8 +862,11 @@ export class DshSdkSession extends EventEmitter {
798
862
  this.rotateSessionId();
799
863
  }
800
864
  const effectivePrompt = this.buildPrompt(promptText, { useInitialImages });
801
- this.history.push({ role: "user", content: promptText });
865
+ if (!suppressReply) {
866
+ this.history.push({ role: "user", content: promptText });
867
+ }
802
868
  const currentTurn = {
869
+ suppressReply,
803
870
  emittedAssistantMessage: false,
804
871
  fullText: "",
805
872
  interrupted: false,
@@ -861,10 +928,10 @@ export class DshSdkSession extends EventEmitter {
861
928
  const responseText = typeof runResult?.finalResponse === "string" && runResult.finalResponse
862
929
  ? runResult.finalResponse
863
930
  : currentTurn.fullText;
864
- if (!currentTurn.emittedAssistantMessage && responseText) {
931
+ if (!suppressReply && !currentTurn.emittedAssistantMessage && responseText) {
865
932
  await this.emitAssistantMessage(responseText);
866
933
  }
867
- if (responseText) {
934
+ if (!suppressReply && responseText) {
868
935
  this.history.push({ role: "assistant", content: responseText });
869
936
  }
870
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
  }
@@ -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
  }
@@ -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;
@@ -879,7 +885,27 @@ export class KimiCliSession extends EventEmitter {
879
885
  }
880
886
  return false;
881
887
  }
882
- 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 } = {}) {
883
909
  if (this.closeRequested || this.closed) {
884
910
  throw this.createSessionClosedError();
885
911
  }
@@ -907,8 +933,12 @@ export class KimiCliSession extends EventEmitter {
907
933
  await this.failPendingTurnStart(error, onProgress);
908
934
  throw error;
909
935
  }
910
- this.history.push({ role: "user", content: promptText });
936
+ if (!suppressReply) {
937
+ this.history.push({ role: "user", content: promptText });
938
+ }
911
939
  const currentTurn = {
940
+ suppressReply,
941
+ compacted: false,
912
942
  fullText: "",
913
943
  bufferedAssistantText: "",
914
944
  thinkText: "",
@@ -950,7 +980,7 @@ export class KimiCliSession extends EventEmitter {
950
980
  ]);
951
981
  currentTurn.settled = true;
952
982
  await this.finalizeAssistantMessage(currentTurn);
953
- if (currentTurn.fullText) {
983
+ if (currentTurn.fullText && !suppressReply) {
954
984
  this.history.push({ role: "assistant", content: currentTurn.fullText });
955
985
  }
956
986
  const normalizedStatus = normalizeText(promptResult?.status);
@@ -979,6 +1009,7 @@ export class KimiCliSession extends EventEmitter {
979
1009
  promptStatus: normalizedStatus || undefined,
980
1010
  contextUsagePercent: this.lastContextUsagePercent,
981
1011
  },
1012
+ compacted: currentTurn.compacted,
982
1013
  };
983
1014
  }
984
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;
@@ -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],
@@ -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: "",
@@ -624,7 +663,9 @@ export class KimiPrintSession extends EventEmitter {
624
663
  }
625
664
  if (assistantText) {
626
665
  currentTurn.fullText += assistantText;
627
- void this.emitAssistantMessage(assistantText);
666
+ if (!suppressReply) {
667
+ void this.emitAssistantMessage(assistantText);
668
+ }
628
669
  }
629
670
  return;
630
671
  }
@@ -688,7 +729,7 @@ export class KimiPrintSession extends EventEmitter {
688
729
  }
689
730
  child.stdin.end();
690
731
  });
691
- if (result?.text) {
732
+ if (result?.text && !suppressReply) {
692
733
  this.history.push({ role: "assistant", content: result.text });
693
734
  }
694
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;