@love-moon/ai-sdk 0.13.2 → 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.
- package/CHANGELOG.md +70 -0
- package/dist/client.d.ts +5 -0
- package/dist/client.js +22 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/providers/chat-web-session.d.ts +18 -0
- package/dist/providers/chat-web-session.js +44 -1
- package/dist/providers/claude-agent-sdk-session.d.ts +34 -0
- package/dist/providers/claude-agent-sdk-session.js +33 -0
- package/dist/providers/codex-app-server-session.d.ts +76 -6
- package/dist/providers/codex-app-server-session.js +135 -21
- package/dist/providers/codex-exec-session.d.ts +14 -0
- package/dist/providers/codex-exec-session.js +20 -1
- package/dist/providers/copilot-sdk-session.d.ts +21 -0
- package/dist/providers/copilot-sdk-session.js +55 -12
- package/dist/providers/dsh-sdk-session.d.ts +53 -3
- package/dist/providers/dsh-sdk-session.js +24 -1
- package/dist/providers/kimi-cli-session.d.ts +22 -0
- package/dist/providers/kimi-cli-session.js +23 -1
- package/dist/providers/kimi-print-session.d.ts +13 -0
- package/dist/providers/kimi-print-session.js +29 -0
- package/dist/providers/kimi-slash-commands.d.ts +1 -0
- package/dist/providers/kimi-slash-commands.js +23 -0
- package/dist/providers/opencode-sdk-session.d.ts +52 -3
- package/dist/providers/opencode-sdk-session.js +33 -1
- package/dist/shared.d.ts +46 -0
- package/dist/shared.js +18 -1
- package/dist/transports/kimi-wire-transport.d.ts +12 -0
- package/dist/transports/kimi-wire-transport.js +40 -1
- package/dist/turn-usage.d.ts +31 -0
- package/dist/turn-usage.js +44 -0
- package/dist/worker.js +1 -1
- 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
|
-
/**
|
|
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);
|
|
@@ -121,7 +153,7 @@ function resolveItemPhase(item) {
|
|
|
121
153
|
includesCandidate("reason", "summary")) {
|
|
122
154
|
return "reasoning";
|
|
123
155
|
}
|
|
124
|
-
if (hasCandidate("message", "agent_message", "output_text", "final_answer")) {
|
|
156
|
+
if (hasCandidate("message", "agentmessage", "agent_message", "output_text", "final_answer")) {
|
|
125
157
|
return "assistant_message";
|
|
126
158
|
}
|
|
127
159
|
if (hasCandidate("compaction", "context_compacted", "compacted") || includesCandidate("compact")) {
|
|
@@ -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
|
-
|
|
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 =
|
|
533
|
+
this.turnTokenBaseline = readTokenCounts(this.tokenUsage?.total);
|
|
496
534
|
this.turnTokensSeen = false;
|
|
497
535
|
}
|
|
498
536
|
/**
|
|
499
|
-
* Thread token usage plus `turnTotalTokens
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
* did has
|
|
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
|
|
547
|
+
const turn = this.turnTokensSeen
|
|
548
|
+
? subtractTokenCounts(readTokenCounts(this.tokenUsage.total), this.turnTokenBaseline)
|
|
549
|
+
: null;
|
|
509
550
|
return {
|
|
510
551
|
...this.tokenUsage,
|
|
511
|
-
...(
|
|
512
|
-
? {
|
|
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
|
}
|
|
@@ -838,8 +907,19 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
838
907
|
if (this.closeRequested) {
|
|
839
908
|
return;
|
|
840
909
|
}
|
|
910
|
+
// Multi-agent mode streams spawned sub-agent threads over this connection.
|
|
911
|
+
// Only our own thread may drive turn/message state: a sub-agent's
|
|
912
|
+
// turn/completed must not end the parent turn and drop its final answer.
|
|
913
|
+
const threadId = typeof params?.threadId === "string" ? params.threadId.trim() : "";
|
|
914
|
+
if (threadId && this.sessionId && threadId !== this.sessionId) {
|
|
915
|
+
this.touchTurnActivity();
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
841
918
|
switch (method) {
|
|
842
919
|
case "thread/started":
|
|
920
|
+
if (params?.thread?.parentThreadId) {
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
843
923
|
this.applyThreadInfo(params, { resumeReady: Boolean(this.resumeSessionId) });
|
|
844
924
|
return;
|
|
845
925
|
case "sessionConfigured":
|
|
@@ -965,12 +1045,16 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
965
1045
|
}
|
|
966
1046
|
return;
|
|
967
1047
|
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1048
|
+
// Notifications from one stdout batch run concurrently. Buffer the
|
|
1049
|
+
// delta before yielding so item/completed cannot flush a partial reply.
|
|
1050
|
+
await Promise.all([
|
|
1051
|
+
this.queueAssistantDelta(delta, { messageId }),
|
|
1052
|
+
this.emitWorkingStatus({
|
|
1053
|
+
phase: "message_aggregation",
|
|
1054
|
+
reply_in_progress: true,
|
|
1055
|
+
status_line: statusLineForPhase("message_aggregation"),
|
|
1056
|
+
}),
|
|
1057
|
+
]);
|
|
974
1058
|
return;
|
|
975
1059
|
}
|
|
976
1060
|
case "thread/tokenUsage/updated": {
|
|
@@ -984,9 +1068,8 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
984
1068
|
// No total was known when the turn started (fresh boot or resume):
|
|
985
1069
|
// a replayed total is the baseline; this turn's first response
|
|
986
1070
|
// has `total - last` before it.
|
|
987
|
-
const total =
|
|
988
|
-
|
|
989
|
-
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));
|
|
990
1073
|
}
|
|
991
1074
|
if (!beforeTurn) {
|
|
992
1075
|
this.turnTokensSeen = true;
|
|
@@ -1187,6 +1270,7 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
1187
1270
|
reason: "turn_already_running",
|
|
1188
1271
|
});
|
|
1189
1272
|
}
|
|
1273
|
+
const startedAt = performance.now();
|
|
1190
1274
|
this.markTurnStartedStatus();
|
|
1191
1275
|
try {
|
|
1192
1276
|
await this.boot();
|
|
@@ -1205,6 +1289,7 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
1205
1289
|
rejectTurn = reject;
|
|
1206
1290
|
});
|
|
1207
1291
|
const currentTurn = {
|
|
1292
|
+
startedAt,
|
|
1208
1293
|
turnId: "",
|
|
1209
1294
|
fullText: "",
|
|
1210
1295
|
activeAssistantMessageId: "",
|
|
@@ -1338,6 +1423,35 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
1338
1423
|
* @param {import("../shared.js").CompactRequest} [request]
|
|
1339
1424
|
* @returns {Promise<import("../shared.js").CompactResult>}
|
|
1340
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
|
+
}
|
|
1341
1455
|
async runCompact(request = {}) {
|
|
1342
1456
|
if (this.closeRequested) {
|
|
1343
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 =
|
|
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
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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();
|