@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.
- package/CHANGELOG.md +82 -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 +114 -14
- 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/resume/claude.d.ts +8 -0
- package/dist/resume/claude.js +28 -1
- package/dist/resume/codex.d.ts +8 -0
- package/dist/resume/codex.js +37 -1
- package/dist/resume/index.d.ts +8 -0
- package/dist/resume/shared.d.ts +18 -0
- package/dist/resume/shared.js +65 -1
- package/dist/shared.d.ts +46 -0
- package/dist/shared.js +18 -1
- package/dist/transports/codex-app-server-transport.js +22 -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);
|
|
@@ -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
|
-
|
|
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 =
|
|
534
|
+
this.turnTokenBaseline = readTokenCounts(this.tokenUsage?.total);
|
|
496
535
|
this.turnTokensSeen = false;
|
|
497
536
|
}
|
|
498
537
|
/**
|
|
499
|
-
* Thread token usage plus `turnTotalTokens
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
* did has
|
|
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
|
|
548
|
+
const turn = this.turnTokensSeen
|
|
549
|
+
? subtractTokenCounts(readTokenCounts(this.tokenUsage.total), this.turnTokenBaseline)
|
|
550
|
+
: null;
|
|
509
551
|
return {
|
|
510
552
|
...this.tokenUsage,
|
|
511
|
-
...(
|
|
512
|
-
? {
|
|
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 =
|
|
1003
|
-
|
|
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 =
|
|
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();
|
|
@@ -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;
|