@love-moon/ai-sdk 0.13.3 → 0.15.0

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/dist/client.d.ts +5 -0
  3. package/dist/client.js +22 -0
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.js +1 -0
  6. package/dist/providers/chat-web-session.d.ts +18 -0
  7. package/dist/providers/chat-web-session.js +44 -1
  8. package/dist/providers/claude-agent-sdk-session.d.ts +34 -0
  9. package/dist/providers/claude-agent-sdk-session.js +33 -0
  10. package/dist/providers/codex-app-server-session.d.ts +76 -6
  11. package/dist/providers/codex-app-server-session.js +114 -14
  12. package/dist/providers/codex-exec-session.d.ts +14 -0
  13. package/dist/providers/codex-exec-session.js +20 -1
  14. package/dist/providers/copilot-sdk-session.d.ts +21 -0
  15. package/dist/providers/copilot-sdk-session.js +55 -12
  16. package/dist/providers/dsh-sdk-session.d.ts +53 -3
  17. package/dist/providers/dsh-sdk-session.js +24 -1
  18. package/dist/providers/kimi-cli-session.d.ts +22 -0
  19. package/dist/providers/kimi-cli-session.js +23 -1
  20. package/dist/providers/kimi-print-session.d.ts +13 -0
  21. package/dist/providers/kimi-print-session.js +29 -0
  22. package/dist/providers/kimi-slash-commands.d.ts +1 -0
  23. package/dist/providers/kimi-slash-commands.js +23 -0
  24. package/dist/providers/opencode-sdk-session.d.ts +52 -3
  25. package/dist/providers/opencode-sdk-session.js +33 -1
  26. package/dist/resume/claude.d.ts +8 -0
  27. package/dist/resume/claude.js +28 -1
  28. package/dist/resume/codex.d.ts +8 -0
  29. package/dist/resume/codex.js +37 -1
  30. package/dist/resume/index.d.ts +8 -0
  31. package/dist/resume/shared.d.ts +18 -0
  32. package/dist/resume/shared.js +65 -1
  33. package/dist/shared.d.ts +46 -0
  34. package/dist/shared.js +18 -1
  35. package/dist/transports/codex-app-server-transport.js +22 -1
  36. package/dist/transports/kimi-wire-transport.d.ts +12 -0
  37. package/dist/transports/kimi-wire-transport.js +40 -1
  38. package/dist/turn-usage.d.ts +31 -0
  39. package/dist/turn-usage.js +44 -0
  40. package/dist/worker.js +1 -1
  41. package/package.json +3 -3
@@ -60,7 +60,39 @@ function readTokenCount(breakdown) {
60
60
  const value = Number(breakdown?.totalTokens);
61
61
  return Number.isFinite(value) ? value : null;
62
62
  }
63
- /** Adds tokens spent outside the reported attempt (an overflowed first try) to a turn result or error. */
63
+ /**
64
+ * The thread counters a turn's usage is a delta of; null when the total is
65
+ * unknown. A breakdown without the input split carries the total alone, so a
66
+ * build that omits it reports no cache share rather than a bogus one.
67
+ */
68
+ function readTokenCounts(breakdown) {
69
+ const totalTokens = readTokenCount(breakdown);
70
+ if (totalTokens === null) {
71
+ return null;
72
+ }
73
+ const inputTokens = Number(breakdown.inputTokens);
74
+ const cachedInputTokens = Number(breakdown.cachedInputTokens);
75
+ return Number.isFinite(inputTokens) && Number.isFinite(cachedInputTokens)
76
+ ? { totalTokens, inputTokens, cachedInputTokens }
77
+ : { totalTokens };
78
+ }
79
+ function subtractTokenCounts(a, b) {
80
+ if (!a || !b) {
81
+ return null;
82
+ }
83
+ const split = a.inputTokens !== undefined && b.inputTokens !== undefined;
84
+ return {
85
+ totalTokens: a.totalTokens - b.totalTokens,
86
+ ...(split
87
+ ? { inputTokens: a.inputTokens - b.inputTokens, cachedInputTokens: a.cachedInputTokens - b.cachedInputTokens }
88
+ : {}),
89
+ };
90
+ }
91
+ /**
92
+ * Adds tokens spent outside the reported attempt (an overflowed first try) to a
93
+ * turn result or error. Only the total grows: the input split, and so the cache
94
+ * share, stays that of the attempt the fresh thread reported.
95
+ */
64
96
  function addTurnTokens(holder, extraTokens) {
65
97
  if (extraTokens > 0 && holder && typeof holder === "object") {
66
98
  const counted = Number(holder.usage?.turnTotalTokens);
@@ -204,6 +236,7 @@ export class CodexAppServerSession extends EventEmitter {
204
236
  static capabilities = Object.freeze({
205
237
  goal: true,
206
238
  compact: true,
239
+ clear: true,
207
240
  media: PROVIDER_MEDIA_CAPABILITIES[CODEX_APP_SERVER_VARIANT],
208
241
  });
209
242
  /**
@@ -217,6 +250,7 @@ export class CodexAppServerSession extends EventEmitter {
217
250
  this.backend = normalizeCodexBackend(backend);
218
251
  this.options = options;
219
252
  this.logger = normalizeLogger(options.logger);
253
+ this.traceReplyLatency = /^(1|true|yes)$/i.test(String(options.env?.CONDUCTOR_DEBUG ?? process.env.CONDUCTOR_DEBUG ?? ""));
220
254
  this.cwd =
221
255
  typeof options.cwd === "string" && options.cwd.trim()
222
256
  ? options.cwd.trim()
@@ -273,6 +307,7 @@ export class CodexAppServerSession extends EventEmitter {
273
307
  },
274
308
  commandLine: options.commandLine,
275
309
  enableGoals: this.goalMode || options.enableGoals === true,
310
+ configOverrides: options.configOverrides,
276
311
  });
277
312
  this.transport.on("notification", ({ method, params }) => {
278
313
  void this.handleNotification(method, params);
@@ -414,8 +449,7 @@ export class CodexAppServerSession extends EventEmitter {
414
449
  this.bootPromise = null;
415
450
  }
416
451
  }
417
- async bootInternal() {
418
- await this.transport.boot();
452
+ buildThreadParams() {
419
453
  const params = {
420
454
  cwd: this.cwd,
421
455
  approvalPolicy: "never",
@@ -426,6 +460,11 @@ export class CodexAppServerSession extends EventEmitter {
426
460
  if (this.options.model) {
427
461
  params.model = this.options.model;
428
462
  }
463
+ return params;
464
+ }
465
+ async bootInternal() {
466
+ await this.transport.boot();
467
+ const params = this.buildThreadParams();
429
468
  let result;
430
469
  if (this.resumeSessionId) {
431
470
  result = await this.transport.request("thread/resume", {
@@ -492,24 +531,35 @@ export class CodexAppServerSession extends EventEmitter {
492
531
  }
493
532
  /** Start counting a turn's tokens from the thread total known right now. */
494
533
  beginTurnTokenAccounting() {
495
- this.turnTokenBaseline = readTokenCount(this.tokenUsage?.total);
534
+ this.turnTokenBaseline = readTokenCounts(this.tokenUsage?.total);
496
535
  this.turnTokensSeen = false;
497
536
  }
498
537
  /**
499
- * Thread token usage plus `turnTotalTokens`: tokens spent since the current
500
- * turn (or goal) started. Codex only reports thread-cumulative totals, and
501
- * only when a response completes, so a turn that ended before any response
502
- * did has unknown usage.
538
+ * Thread token usage plus `turnTotalTokens` (and its `turnInputTokens` /
539
+ * `turnCachedInputTokens` split): tokens spent since the current turn (or
540
+ * goal) started. Codex only reports thread-cumulative totals, and only when
541
+ * a response completes, so a turn that ended before any response did has
542
+ * unknown usage.
503
543
  */
504
544
  snapshotTokenUsage() {
505
545
  if (!this.tokenUsage) {
506
546
  return null;
507
547
  }
508
- const total = readTokenCount(this.tokenUsage.total);
548
+ const turn = this.turnTokensSeen
549
+ ? subtractTokenCounts(readTokenCounts(this.tokenUsage.total), this.turnTokenBaseline)
550
+ : null;
509
551
  return {
510
552
  ...this.tokenUsage,
511
- ...(total !== null && this.turnTokenBaseline !== null && this.turnTokensSeen
512
- ? { turnTotalTokens: Math.max(0, total - this.turnTokenBaseline) }
553
+ ...(turn
554
+ ? {
555
+ turnTotalTokens: Math.max(0, turn.totalTokens),
556
+ ...(turn.inputTokens !== undefined
557
+ ? {
558
+ turnInputTokens: Math.max(0, turn.inputTokens),
559
+ turnCachedInputTokens: Math.max(0, turn.cachedInputTokens),
560
+ }
561
+ : {}),
562
+ }
513
563
  : {}),
514
564
  };
515
565
  }
@@ -646,6 +696,8 @@ export class CodexAppServerSession extends EventEmitter {
646
696
  return false;
647
697
  }
648
698
  const text = currentTurn.activeAssistantMessageText || "";
699
+ const firstDeltaAt = currentTurn.activeAssistantFirstDeltaAt;
700
+ currentTurn.activeAssistantFirstDeltaAt = undefined;
649
701
  currentTurn.activeAssistantMessageText = "";
650
702
  if (!finalizedMessageId || !activeMessageId || activeMessageId === finalizedMessageId) {
651
703
  currentTurn.activeAssistantMessageId = "";
@@ -653,6 +705,13 @@ export class CodexAppServerSession extends EventEmitter {
653
705
  if (!text || currentTurn.suppressReply) {
654
706
  return false;
655
707
  }
708
+ if (this.traceReplyLatency && firstDeltaAt !== undefined) {
709
+ this.writeLog(`[codex-reply-latency] ${JSON.stringify({
710
+ event: "reply_emit", replyTo: this.getCurrentReplyTarget(),
711
+ turnId: currentTurn.turnId, itemId: finalizedMessageId,
712
+ firstDeltaToEmitMs: performance.now() - firstDeltaAt,
713
+ })}`);
714
+ }
656
715
  await this.emitAssistantMessage(text);
657
716
  return true;
658
717
  }
@@ -831,6 +890,17 @@ export class CodexAppServerSession extends EventEmitter {
831
890
  if (normalizedMessageId) {
832
891
  currentTurn.activeAssistantMessageId = normalizedMessageId;
833
892
  }
893
+ if (this.traceReplyLatency && delta && currentTurn.activeAssistantFirstDeltaAt === undefined) {
894
+ const firstDeltaAt = performance.now();
895
+ currentTurn.activeAssistantFirstDeltaAt = firstDeltaAt;
896
+ if (!currentTurn.suppressReply) {
897
+ this.writeLog(`[codex-reply-latency] ${JSON.stringify({
898
+ event: "first_delta", replyTo: this.getCurrentReplyTarget(),
899
+ turnId: currentTurn.turnId, itemId: normalizedMessageId,
900
+ turnStartToDeltaMs: currentTurn.startedAt === undefined ? null : firstDeltaAt - currentTurn.startedAt,
901
+ })}`);
902
+ }
903
+ }
834
904
  currentTurn.fullText += delta;
835
905
  currentTurn.activeAssistantMessageText += delta;
836
906
  }
@@ -999,9 +1069,8 @@ export class CodexAppServerSession extends EventEmitter {
999
1069
  // No total was known when the turn started (fresh boot or resume):
1000
1070
  // a replayed total is the baseline; this turn's first response
1001
1071
  // has `total - last` before it.
1002
- const total = readTokenCount(tokenUsage.total);
1003
- const last = readTokenCount(tokenUsage.last);
1004
- this.turnTokenBaseline = total === null ? null : beforeTurn ? total : last === null ? null : total - last;
1072
+ const total = readTokenCounts(tokenUsage.total);
1073
+ this.turnTokenBaseline = beforeTurn ? total : subtractTokenCounts(total, readTokenCounts(tokenUsage.last));
1005
1074
  }
1006
1075
  if (!beforeTurn) {
1007
1076
  this.turnTokensSeen = true;
@@ -1202,6 +1271,7 @@ export class CodexAppServerSession extends EventEmitter {
1202
1271
  reason: "turn_already_running",
1203
1272
  });
1204
1273
  }
1274
+ const startedAt = performance.now();
1205
1275
  this.markTurnStartedStatus();
1206
1276
  try {
1207
1277
  await this.boot();
@@ -1220,6 +1290,7 @@ export class CodexAppServerSession extends EventEmitter {
1220
1290
  rejectTurn = reject;
1221
1291
  });
1222
1292
  const currentTurn = {
1293
+ startedAt,
1223
1294
  turnId: "",
1224
1295
  fullText: "",
1225
1296
  activeAssistantMessageId: "",
@@ -1353,6 +1424,35 @@ export class CodexAppServerSession extends EventEmitter {
1353
1424
  * @param {import("../shared.js").CompactRequest} [request]
1354
1425
  * @returns {Promise<import("../shared.js").CompactResult>}
1355
1426
  */
1427
+ /**
1428
+ * Native clear: start a fresh thread on the SAME app-server connection. The
1429
+ * process, transport and model config stay up; only the conversation is
1430
+ * replaced, so the next turn runs against an empty thread.
1431
+ */
1432
+ async runClear() {
1433
+ if (this.closeRequested) {
1434
+ throw this.createSessionClosedError();
1435
+ }
1436
+ if (this.currentTurn || this.currentGoalRun) {
1437
+ throw createTurnError("Codex app-server turn already running", {
1438
+ reason: "turn_already_running",
1439
+ });
1440
+ }
1441
+ if (this.pendingHistorySeed || (!this.resumeSessionId && !this.sessionId && this.history.length === 0)) {
1442
+ return { clear: { status: "noop" }, usage: null, metadata: {} };
1443
+ }
1444
+ await this.boot();
1445
+ const result = await this.transport.request("thread/start", this.buildThreadParams());
1446
+ this.applyThreadInfo(result);
1447
+ // A later re-boot must not resume the thread we just walked away from.
1448
+ this.resumeSessionId = "";
1449
+ this.history = [];
1450
+ return {
1451
+ clear: { status: "cleared", sessionId: this.sessionId || undefined },
1452
+ usage: null,
1453
+ metadata: {},
1454
+ };
1455
+ }
1356
1456
  async runCompact(request = {}) {
1357
1457
  if (this.closeRequested) {
1358
1458
  throw this.createSessionClosedError();
@@ -53,6 +53,7 @@ export class CodexExecSession extends EventEmitter<[never]> {
53
53
  manualResume: null;
54
54
  currentTurnStatus: any;
55
55
  capabilities: {
56
+ clear: boolean;
56
57
  media: Readonly<{
57
58
  image: "native";
58
59
  }>;
@@ -90,6 +91,19 @@ export class CodexExecSession extends EventEmitter<[never]> {
90
91
  emitWorkingStatus(payload: any, onProgress?: null): Promise<any>;
91
92
  emitAssistantMessage(text: any): Promise<void>;
92
93
  buildPrompt(promptText: any): string;
94
+ /**
95
+ * Native clear: `codex exec` is stateless — every turn replays this history
96
+ * as the prompt prefix (see buildPrompt), so dropping it IS the context
97
+ * clear. No process to restart, no session to rebind.
98
+ */
99
+ runClear(): Promise<{
100
+ clear: {
101
+ status: string;
102
+ sessionId: any;
103
+ };
104
+ usage: null;
105
+ metadata: {};
106
+ }>;
93
107
  buildExecArgs({ media, schemaFilePath, lastMessageFilePath }?: {
94
108
  media?: never[] | undefined;
95
109
  schemaFilePath?: string | undefined;
@@ -242,7 +242,7 @@ export class CodexExecSession extends EventEmitter {
242
242
  resumeReady: false,
243
243
  manualResume: null,
244
244
  currentTurnStatus: this.getCurrentTurnStatus(),
245
- capabilities: { media: PROVIDER_MEDIA_CAPABILITIES[CODEX_EXEC_PROVIDER_VARIANT] },
245
+ capabilities: { clear: true, media: PROVIDER_MEDIA_CAPABILITIES[CODEX_EXEC_PROVIDER_VARIANT] },
246
246
  pid: this.currentTurn?.child?.pid || undefined,
247
247
  };
248
248
  }
@@ -342,6 +342,25 @@ export class CodexExecSession extends EventEmitter {
342
342
  buildPrompt(promptText) {
343
343
  return buildHistoryPrompt(this.history, promptText);
344
344
  }
345
+ /**
346
+ * Native clear: `codex exec` is stateless — every turn replays this history
347
+ * as the prompt prefix (see buildPrompt), so dropping it IS the context
348
+ * clear. No process to restart, no session to rebind.
349
+ */
350
+ async runClear() {
351
+ if (this.currentTurn) {
352
+ throw createTurnError("Codex exec turn already running", {
353
+ reason: "turn_already_running",
354
+ });
355
+ }
356
+ const hadContext = this.history.length > 0;
357
+ this.history = [];
358
+ return {
359
+ clear: { status: hadContext ? "cleared" : "noop", sessionId: this.sessionId || undefined },
360
+ usage: null,
361
+ metadata: {},
362
+ };
363
+ }
345
364
  buildExecArgs({ media = [], schemaFilePath = "", lastMessageFilePath = "" } = {}) {
346
365
  const args = [...this.baseArgs];
347
366
  args.push("exec");
@@ -85,6 +85,7 @@ export class CopilotSdkSession extends EventEmitter<[never]> {
85
85
  currentTurnStatus: any;
86
86
  capabilities: {
87
87
  compact: boolean;
88
+ clear: boolean;
88
89
  media: Readonly<{
89
90
  image: "native";
90
91
  }>;
@@ -147,6 +148,26 @@ export class CopilotSdkSession extends EventEmitter<[never]> {
147
148
  getSdkModule(): Promise<any>;
148
149
  boot(): Promise<void>;
149
150
  bootInternal(): Promise<void>;
151
+ /**
152
+ * Native clear: create a new session on the SAME CopilotClient, so the CLI
153
+ * subprocess keeps running. The old conversation is left on disk (resumable)
154
+ * and we simply stop listening to it.
155
+ */
156
+ runClear(): Promise<{
157
+ clear: {
158
+ status: string;
159
+ sessionId?: undefined;
160
+ };
161
+ usage: null;
162
+ metadata: {};
163
+ } | {
164
+ clear: {
165
+ status: string;
166
+ sessionId: any;
167
+ };
168
+ usage: null;
169
+ metadata: {};
170
+ }>;
150
171
  attachSessionEventHandlers(session: any): void;
151
172
  detachSessionEventHandlers(): void;
152
173
  requestOrThrow(promise: any): Promise<any>;
@@ -505,6 +505,21 @@ function buildCopilotSessionConfig(options, cwd, permissionHandler) {
505
505
  }
506
506
  return sessionConfig;
507
507
  }
508
+ function resolveCopilotPermissionHandler(sdkModule) {
509
+ return typeof sdkModule?.approveAll === "function"
510
+ ? sdkModule.approveAll
511
+ : () => ({ kind: "approve-once" });
512
+ }
513
+ function sessionInfoExtrasFromConfig(sessionConfig = {}) {
514
+ return {
515
+ model: typeof sessionConfig.model === "string" && sessionConfig.model.trim()
516
+ ? sessionConfig.model.trim()
517
+ : undefined,
518
+ reasoningEffort: typeof sessionConfig.reasoningEffort === "string" && sessionConfig.reasoningEffort.trim()
519
+ ? sessionConfig.reasoningEffort.trim()
520
+ : undefined,
521
+ };
522
+ }
508
523
  export class CopilotSdkSession extends EventEmitter {
509
524
  constructor(backend, options = {}) {
510
525
  super();
@@ -594,7 +609,7 @@ export class CopilotSdkSession extends EventEmitter {
594
609
  }
595
610
  : null,
596
611
  currentTurnStatus: this.getCurrentTurnStatus(),
597
- capabilities: { compact: true, media: PROVIDER_MEDIA_CAPABILITIES[COPILOT_PROVIDER_VARIANT] },
612
+ capabilities: { compact: true, clear: true, media: PROVIDER_MEDIA_CAPABILITIES[COPILOT_PROVIDER_VARIANT] },
598
613
  };
599
614
  }
600
615
  getSessionInfo() {
@@ -910,9 +925,7 @@ export class CopilotSdkSession extends EventEmitter {
910
925
  if (!sdkModule || typeof sdkModule.CopilotClient !== "function") {
911
926
  throw new Error("GitHub Copilot SDK client is unavailable");
912
927
  }
913
- const permissionHandler = typeof sdkModule.approveAll === "function"
914
- ? sdkModule.approveAll
915
- : () => ({ kind: "approve-once" });
928
+ const permissionHandler = resolveCopilotPermissionHandler(sdkModule);
916
929
  const clientOptions = buildCopilotClientOptions(this.options, this.cwd, this.env);
917
930
  this.client = new sdkModule.CopilotClient(clientOptions);
918
931
  const cleanupIfClosedDuringBoot = async (session = null) => {
@@ -941,14 +954,44 @@ export class CopilotSdkSession extends EventEmitter {
941
954
  : await this.requestOrThrow(this.client.createSession(sessionConfig));
942
955
  await cleanupIfClosedDuringBoot(this.session);
943
956
  this.attachSessionEventHandlers(this.session);
944
- this.applySessionInfo(this.session?.sessionId, {
945
- model: typeof sessionConfig.model === "string" && sessionConfig.model.trim()
946
- ? sessionConfig.model.trim()
947
- : undefined,
948
- reasoningEffort: typeof sessionConfig.reasoningEffort === "string" && sessionConfig.reasoningEffort.trim()
949
- ? sessionConfig.reasoningEffort.trim()
950
- : undefined,
951
- });
957
+ this.applySessionInfo(this.session?.sessionId, sessionInfoExtrasFromConfig(sessionConfig));
958
+ }
959
+ /**
960
+ * Native clear: create a new session on the SAME CopilotClient, so the CLI
961
+ * subprocess keeps running. The old conversation is left on disk (resumable)
962
+ * and we simply stop listening to it.
963
+ */
964
+ async runClear() {
965
+ if (this.currentTurn) {
966
+ throw this.createTurnAlreadyRunningError();
967
+ }
968
+ if (this.pendingHistorySeed || (!this.session && !this.resumeSessionId && !this.sessionId)) {
969
+ return { clear: { status: "noop" }, usage: null, metadata: {} };
970
+ }
971
+ await this.boot();
972
+ const sdkModule = await this.getSdkModule();
973
+ const sessionConfig = buildCopilotSessionConfig(this.options, this.cwd, resolveCopilotPermissionHandler(sdkModule));
974
+ const previousSession = this.session;
975
+ const session = await this.requestOrThrow(this.client.createSession(sessionConfig));
976
+ this.session = session;
977
+ this.attachSessionEventHandlers(session);
978
+ this.applySessionInfo(session?.sessionId, sessionInfoExtrasFromConfig(sessionConfig));
979
+ // A later re-boot must not reattach to the conversation we walked away from.
980
+ this.resumeSessionId = "";
981
+ this.history = [];
982
+ if (previousSession && previousSession !== session && typeof previousSession.disconnect === "function") {
983
+ try {
984
+ await previousSession.disconnect();
985
+ }
986
+ catch {
987
+ // best effort: the old conversation is already detached from this session
988
+ }
989
+ }
990
+ return {
991
+ clear: { status: "cleared", sessionId: this.sessionId || undefined },
992
+ usage: null,
993
+ metadata: {},
994
+ };
952
995
  }
953
996
  attachSessionEventHandlers(session) {
954
997
  if (!session || typeof session.on !== "function") {
@@ -19,11 +19,13 @@ export class DshSdkSession extends EventEmitter<[never]> {
19
19
  static capabilities: Readonly<{
20
20
  goal: false;
21
21
  compact: true;
22
+ clear: true;
22
23
  }>;
23
24
  constructor(backend: any, options?: {});
24
25
  getCapabilities(): {
25
26
  goal: false;
26
27
  compact: true;
28
+ clear: true;
27
29
  };
28
30
  backend: string;
29
31
  options: {};
@@ -92,6 +94,7 @@ export class DshSdkSession extends EventEmitter<[never]> {
92
94
  capabilities: {
93
95
  goal: false;
94
96
  compact: true;
97
+ clear: true;
95
98
  };
96
99
  };
97
100
  getSessionInfo(): {
@@ -180,9 +183,56 @@ export class DshSdkSession extends EventEmitter<[never]> {
180
183
  * @param {{ onProgress?: Function }} [options]
181
184
  * @returns {Promise<import("../shared.js").CompactResult>}
182
185
  */
183
- runCompact(request?: import("../shared.js").CompactRequest, { onProgress }?: {
184
- onProgress?: Function;
185
- }): Promise<import("../shared.js").CompactResult>;
186
+ /**
187
+ * Native clear: drop the conversation and rotate onto a fresh wire session id.
188
+ * The harness (and its runtime subprocess) stays up — the dsh runtime creates
189
+ * the new session lazily on its first prompt — so this costs no model call and
190
+ * no restart. Unlike runCompact there is nothing to carry over, so rotating
191
+ * here is safe: the new id has no persisted log, and resuming it restores an
192
+ * empty conversation, which is exactly what `/clear` promises.
193
+ */
194
+ runClear(): Promise<{
195
+ clear: {
196
+ status: string;
197
+ sessionId?: undefined;
198
+ };
199
+ usage: null;
200
+ metadata: {
201
+ source?: undefined;
202
+ };
203
+ } | {
204
+ clear: {
205
+ status: string;
206
+ sessionId: string;
207
+ };
208
+ usage: null;
209
+ metadata: {
210
+ source: string;
211
+ };
212
+ }>;
213
+ runCompact(request?: {}, { onProgress }?: {
214
+ onProgress?: null | undefined;
215
+ }): Promise<{
216
+ compact: {
217
+ status: string;
218
+ instructionsApplied: boolean;
219
+ };
220
+ usage: null;
221
+ metadata: {
222
+ source?: undefined;
223
+ sessionId?: undefined;
224
+ };
225
+ } | {
226
+ compact: {
227
+ status: string;
228
+ instructionsApplied: boolean;
229
+ };
230
+ usage: any;
231
+ metadata: {
232
+ source: string;
233
+ sessionId: string;
234
+ };
235
+ }>;
186
236
  runTurn(promptText: any, { useInitialImages, onProgress, suppressReply }?: {
187
237
  useInitialImages?: boolean | undefined;
188
238
  onProgress?: null | undefined;
@@ -132,7 +132,7 @@ function accumulateUsage(total, usage) {
132
132
  * the next runTurn spawns a fresh runtime resuming the same session id.
133
133
  */
134
134
  export class DshSdkSession extends EventEmitter {
135
- static capabilities = Object.freeze({ goal: false, compact: true });
135
+ static capabilities = Object.freeze({ goal: false, compact: true, clear: true });
136
136
  getCapabilities() {
137
137
  return { ...DshSdkSession.capabilities };
138
138
  }
@@ -807,6 +807,29 @@ export class DshSdkSession extends EventEmitter {
807
807
  * @param {{ onProgress?: Function }} [options]
808
808
  * @returns {Promise<import("../shared.js").CompactResult>}
809
809
  */
810
+ /**
811
+ * Native clear: drop the conversation and rotate onto a fresh wire session id.
812
+ * The harness (and its runtime subprocess) stays up — the dsh runtime creates
813
+ * the new session lazily on its first prompt — so this costs no model call and
814
+ * no restart. Unlike runCompact there is nothing to carry over, so rotating
815
+ * here is safe: the new id has no persisted log, and resuming it restores an
816
+ * empty conversation, which is exactly what `/clear` promises.
817
+ */
818
+ async runClear() {
819
+ const resumableLog = this.pendingResumeFromSessionId && this.findPersistedSessionLog(this.pendingResumeFromSessionId);
820
+ if (this.history.length === 0 && !resumableLog) {
821
+ return { clear: { status: "noop" }, usage: null, metadata: {} };
822
+ }
823
+ this.history = [];
824
+ // Without this the next turn would restore the old session's log.
825
+ this.pendingResumeFromSessionId = "";
826
+ this.rotateSessionId();
827
+ return {
828
+ clear: { status: "cleared", sessionId: this.sessionId },
829
+ usage: null,
830
+ metadata: { source: DSH_SDK_VARIANT },
831
+ };
832
+ }
810
833
  async runCompact(request = {}, { onProgress = null } = {}) {
811
834
  const instructions = typeof request?.instructions === "string" ? request.instructions.trim() : "";
812
835
  const resumableLog = this.pendingResumeFromSessionId && this.findPersistedSessionLog(this.pendingResumeFromSessionId);
@@ -87,6 +87,7 @@ export class KimiCliSession extends EventEmitter<[never]> {
87
87
  currentTurnStatus: any;
88
88
  capabilities: {
89
89
  compact: boolean;
90
+ clear: boolean;
90
91
  media: Readonly<{
91
92
  image: "native";
92
93
  }>;
@@ -169,6 +170,27 @@ export class KimiCliSession extends EventEmitter<[never]> {
169
170
  runCompact(request?: import("../shared.js").CompactRequest, { onProgress }?: {
170
171
  onProgress?: Function;
171
172
  }): Promise<import("../shared.js").CompactResult>;
173
+ /**
174
+ * Native `/clear`: the Kimi CLI answers the slash command locally (no model
175
+ * call) and keeps the same wire session, so only the context is dropped.
176
+ */
177
+ runClear(request?: {}, { onProgress }?: {
178
+ onProgress?: null | undefined;
179
+ }): Promise<{
180
+ clear: {
181
+ status: string;
182
+ sessionId?: undefined;
183
+ };
184
+ usage: null;
185
+ metadata: {};
186
+ } | {
187
+ clear: {
188
+ status: string;
189
+ sessionId: any;
190
+ };
191
+ usage: null;
192
+ metadata: any;
193
+ }>;
172
194
  runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, onProgress, jsonSchema, suppressReply }?: {
173
195
  useInitialImages?: boolean | undefined;
174
196
  onProgress?: null | undefined;
@@ -5,6 +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 { assertKimiClearReply } from "./kimi-slash-commands.js";
8
9
  import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, noteToolFinished, noteToolStarted, proxyToEnv, sanitizeForLog, withActiveTool, } from "../shared.js";
9
10
  const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
10
11
  const MIN_TURN_DEADLINE_MS = 30 * 1000;
@@ -233,7 +234,7 @@ export class KimiCliSession extends EventEmitter {
233
234
  }
234
235
  : null,
235
236
  currentTurnStatus: this.getCurrentTurnStatus(),
236
- capabilities: { compact: true, media: PROVIDER_MEDIA_CAPABILITIES[KIMI_PROVIDER_VARIANT] },
237
+ capabilities: { compact: true, clear: true, media: PROVIDER_MEDIA_CAPABILITIES[KIMI_PROVIDER_VARIANT] },
237
238
  pid: this.transport.pid || undefined,
238
239
  };
239
240
  }
@@ -905,6 +906,27 @@ export class KimiCliSession extends EventEmitter {
905
906
  metadata: turnResult.metadata,
906
907
  };
907
908
  }
909
+ /**
910
+ * Native `/clear`: the Kimi CLI answers the slash command locally (no model
911
+ * call) and keeps the same wire session, so only the context is dropped.
912
+ */
913
+ async runClear(request = {}, { onProgress = null } = {}) {
914
+ if (this.pendingHistorySeed) {
915
+ return { clear: { status: "noop" }, usage: null, metadata: {} };
916
+ }
917
+ const turnResult = await this.runTurn("/clear", { onProgress, suppressReply: true });
918
+ assertKimiClearReply(turnResult.text);
919
+ this.history = [];
920
+ // The previous turn's context reading describes a context that no longer
921
+ // exists; leave it unknown until kimi reports the next StatusUpdate.
922
+ this.lastContextUsagePercent = undefined;
923
+ return {
924
+ clear: { status: "cleared", sessionId: this.sessionId || undefined },
925
+ // A built-in slash command spends no tokens of its own.
926
+ usage: null,
927
+ metadata: turnResult.metadata,
928
+ };
929
+ }
908
930
  async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null, jsonSchema = null, suppressReply = false } = {}) {
909
931
  if (this.closeRequested || this.closed) {
910
932
  throw this.createSessionClosedError();
@@ -62,6 +62,7 @@ export class KimiPrintSession extends EventEmitter<[never]> {
62
62
  currentTurnStatus: any;
63
63
  capabilities: {
64
64
  compact: boolean;
65
+ clear: boolean;
65
66
  media: Readonly<{
66
67
  image: "unsupported";
67
68
  }> | Readonly<{
@@ -129,6 +130,18 @@ export class KimiPrintSession extends EventEmitter<[never]> {
129
130
  runCompact(request?: import("../shared.js").CompactRequest, { onProgress }?: {
130
131
  onProgress?: Function;
131
132
  }): Promise<import("../shared.js").CompactResult>;
133
+ /**
134
+ * Native `/clear`: kimi-cli's built-in slash command, answered locally without
135
+ * a model call. Print mode spawns a child per turn, so nothing is torn down;
136
+ * the `--session` id stays the same and only its context file rotates.
137
+ *
138
+ * @param {Record<string, unknown>} [request]
139
+ * @param {{ onProgress?: Function }} [options]
140
+ * @returns {Promise<import("../shared.js").ClearResult>}
141
+ */
142
+ runClear(request?: Record<string, unknown>, { onProgress }?: {
143
+ onProgress?: Function;
144
+ }): Promise<import("../shared.js").ClearResult>;
132
145
  runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, onProgress, jsonSchema, suppressReply }?: {
133
146
  useInitialImages?: boolean | undefined;
134
147
  onProgress?: null | undefined;