@love-moon/ai-sdk 0.13.3 → 0.14.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 (33) hide show
  1. package/CHANGELOG.md +55 -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 +113 -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/shared.d.ts +46 -0
  27. package/dist/shared.js +18 -1
  28. package/dist/transports/kimi-wire-transport.d.ts +12 -0
  29. package/dist/transports/kimi-wire-transport.js +40 -1
  30. package/dist/turn-usage.d.ts +31 -0
  31. package/dist/turn-usage.js +44 -0
  32. package/dist/worker.js +1 -1
  33. 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()
@@ -414,8 +448,7 @@ export class CodexAppServerSession extends EventEmitter {
414
448
  this.bootPromise = null;
415
449
  }
416
450
  }
417
- async bootInternal() {
418
- await this.transport.boot();
451
+ buildThreadParams() {
419
452
  const params = {
420
453
  cwd: this.cwd,
421
454
  approvalPolicy: "never",
@@ -426,6 +459,11 @@ export class CodexAppServerSession extends EventEmitter {
426
459
  if (this.options.model) {
427
460
  params.model = this.options.model;
428
461
  }
462
+ return params;
463
+ }
464
+ async bootInternal() {
465
+ await this.transport.boot();
466
+ const params = this.buildThreadParams();
429
467
  let result;
430
468
  if (this.resumeSessionId) {
431
469
  result = await this.transport.request("thread/resume", {
@@ -492,24 +530,35 @@ export class CodexAppServerSession extends EventEmitter {
492
530
  }
493
531
  /** Start counting a turn's tokens from the thread total known right now. */
494
532
  beginTurnTokenAccounting() {
495
- this.turnTokenBaseline = readTokenCount(this.tokenUsage?.total);
533
+ this.turnTokenBaseline = readTokenCounts(this.tokenUsage?.total);
496
534
  this.turnTokensSeen = false;
497
535
  }
498
536
  /**
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.
537
+ * Thread token usage plus `turnTotalTokens` (and its `turnInputTokens` /
538
+ * `turnCachedInputTokens` split): tokens spent since the current turn (or
539
+ * goal) started. Codex only reports thread-cumulative totals, and only when
540
+ * a response completes, so a turn that ended before any response did has
541
+ * unknown usage.
503
542
  */
504
543
  snapshotTokenUsage() {
505
544
  if (!this.tokenUsage) {
506
545
  return null;
507
546
  }
508
- const total = readTokenCount(this.tokenUsage.total);
547
+ const turn = this.turnTokensSeen
548
+ ? subtractTokenCounts(readTokenCounts(this.tokenUsage.total), this.turnTokenBaseline)
549
+ : null;
509
550
  return {
510
551
  ...this.tokenUsage,
511
- ...(total !== null && this.turnTokenBaseline !== null && this.turnTokensSeen
512
- ? { turnTotalTokens: Math.max(0, total - this.turnTokenBaseline) }
552
+ ...(turn
553
+ ? {
554
+ turnTotalTokens: Math.max(0, turn.totalTokens),
555
+ ...(turn.inputTokens !== undefined
556
+ ? {
557
+ turnInputTokens: Math.max(0, turn.inputTokens),
558
+ turnCachedInputTokens: Math.max(0, turn.cachedInputTokens),
559
+ }
560
+ : {}),
561
+ }
513
562
  : {}),
514
563
  };
515
564
  }
@@ -646,6 +695,8 @@ export class CodexAppServerSession extends EventEmitter {
646
695
  return false;
647
696
  }
648
697
  const text = currentTurn.activeAssistantMessageText || "";
698
+ const firstDeltaAt = currentTurn.activeAssistantFirstDeltaAt;
699
+ currentTurn.activeAssistantFirstDeltaAt = undefined;
649
700
  currentTurn.activeAssistantMessageText = "";
650
701
  if (!finalizedMessageId || !activeMessageId || activeMessageId === finalizedMessageId) {
651
702
  currentTurn.activeAssistantMessageId = "";
@@ -653,6 +704,13 @@ export class CodexAppServerSession extends EventEmitter {
653
704
  if (!text || currentTurn.suppressReply) {
654
705
  return false;
655
706
  }
707
+ if (this.traceReplyLatency && firstDeltaAt !== undefined) {
708
+ this.writeLog(`[codex-reply-latency] ${JSON.stringify({
709
+ event: "reply_emit", replyTo: this.getCurrentReplyTarget(),
710
+ turnId: currentTurn.turnId, itemId: finalizedMessageId,
711
+ firstDeltaToEmitMs: performance.now() - firstDeltaAt,
712
+ })}`);
713
+ }
656
714
  await this.emitAssistantMessage(text);
657
715
  return true;
658
716
  }
@@ -831,6 +889,17 @@ export class CodexAppServerSession extends EventEmitter {
831
889
  if (normalizedMessageId) {
832
890
  currentTurn.activeAssistantMessageId = normalizedMessageId;
833
891
  }
892
+ if (this.traceReplyLatency && delta && currentTurn.activeAssistantFirstDeltaAt === undefined) {
893
+ const firstDeltaAt = performance.now();
894
+ currentTurn.activeAssistantFirstDeltaAt = firstDeltaAt;
895
+ if (!currentTurn.suppressReply) {
896
+ this.writeLog(`[codex-reply-latency] ${JSON.stringify({
897
+ event: "first_delta", replyTo: this.getCurrentReplyTarget(),
898
+ turnId: currentTurn.turnId, itemId: normalizedMessageId,
899
+ turnStartToDeltaMs: currentTurn.startedAt === undefined ? null : firstDeltaAt - currentTurn.startedAt,
900
+ })}`);
901
+ }
902
+ }
834
903
  currentTurn.fullText += delta;
835
904
  currentTurn.activeAssistantMessageText += delta;
836
905
  }
@@ -999,9 +1068,8 @@ export class CodexAppServerSession extends EventEmitter {
999
1068
  // No total was known when the turn started (fresh boot or resume):
1000
1069
  // a replayed total is the baseline; this turn's first response
1001
1070
  // 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;
1071
+ const total = readTokenCounts(tokenUsage.total);
1072
+ this.turnTokenBaseline = beforeTurn ? total : subtractTokenCounts(total, readTokenCounts(tokenUsage.last));
1005
1073
  }
1006
1074
  if (!beforeTurn) {
1007
1075
  this.turnTokensSeen = true;
@@ -1202,6 +1270,7 @@ export class CodexAppServerSession extends EventEmitter {
1202
1270
  reason: "turn_already_running",
1203
1271
  });
1204
1272
  }
1273
+ const startedAt = performance.now();
1205
1274
  this.markTurnStartedStatus();
1206
1275
  try {
1207
1276
  await this.boot();
@@ -1220,6 +1289,7 @@ export class CodexAppServerSession extends EventEmitter {
1220
1289
  rejectTurn = reject;
1221
1290
  });
1222
1291
  const currentTurn = {
1292
+ startedAt,
1223
1293
  turnId: "",
1224
1294
  fullText: "",
1225
1295
  activeAssistantMessageId: "",
@@ -1353,6 +1423,35 @@ export class CodexAppServerSession extends EventEmitter {
1353
1423
  * @param {import("../shared.js").CompactRequest} [request]
1354
1424
  * @returns {Promise<import("../shared.js").CompactResult>}
1355
1425
  */
1426
+ /**
1427
+ * Native clear: start a fresh thread on the SAME app-server connection. The
1428
+ * process, transport and model config stay up; only the conversation is
1429
+ * replaced, so the next turn runs against an empty thread.
1430
+ */
1431
+ async runClear() {
1432
+ if (this.closeRequested) {
1433
+ throw this.createSessionClosedError();
1434
+ }
1435
+ if (this.currentTurn || this.currentGoalRun) {
1436
+ throw createTurnError("Codex app-server turn already running", {
1437
+ reason: "turn_already_running",
1438
+ });
1439
+ }
1440
+ if (this.pendingHistorySeed || (!this.resumeSessionId && !this.sessionId && this.history.length === 0)) {
1441
+ return { clear: { status: "noop" }, usage: null, metadata: {} };
1442
+ }
1443
+ await this.boot();
1444
+ const result = await this.transport.request("thread/start", this.buildThreadParams());
1445
+ this.applyThreadInfo(result);
1446
+ // A later re-boot must not resume the thread we just walked away from.
1447
+ this.resumeSessionId = "";
1448
+ this.history = [];
1449
+ return {
1450
+ clear: { status: "cleared", sessionId: this.sessionId || undefined },
1451
+ usage: null,
1452
+ metadata: {},
1453
+ };
1454
+ }
1356
1455
  async runCompact(request = {}) {
1357
1456
  if (this.closeRequested) {
1358
1457
  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;