@love-moon/ai-sdk 0.13.1 → 0.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/dist/client.d.ts +5 -0
- package/dist/client.js +24 -0
- package/dist/providers/claude-agent-sdk-session.d.ts +25 -1
- package/dist/providers/claude-agent-sdk-session.js +96 -7
- package/dist/providers/codex-app-server-session.d.ts +44 -0
- package/dist/providers/codex-app-server-session.js +199 -17
- package/dist/providers/copilot-sdk-session.d.ts +29 -1
- package/dist/providers/copilot-sdk-session.js +111 -16
- package/dist/providers/dsh-sdk-session.d.ts +20 -1
- package/dist/providers/dsh-sdk-session.js +75 -8
- package/dist/providers/kimi-cli-session.d.ts +18 -1
- package/dist/providers/kimi-cli-session.js +36 -5
- package/dist/providers/kimi-print-session.d.ts +16 -1
- package/dist/providers/kimi-print-session.js +45 -4
- package/dist/providers/opencode-sdk-session.d.ts +44 -2
- package/dist/providers/opencode-sdk-session.js +169 -24
- package/dist/shared.d.ts +66 -0
- package/dist/shared.js +25 -1
- package/dist/worker.js +1 -1
- package/package.json +3 -3
|
@@ -56,6 +56,24 @@ ${schemaText}
|
|
|
56
56
|
|
|
57
57
|
${promptText}`;
|
|
58
58
|
}
|
|
59
|
+
function readTokenCount(breakdown) {
|
|
60
|
+
const value = Number(breakdown?.totalTokens);
|
|
61
|
+
return Number.isFinite(value) ? value : null;
|
|
62
|
+
}
|
|
63
|
+
/** Adds tokens spent outside the reported attempt (an overflowed first try) to a turn result or error. */
|
|
64
|
+
function addTurnTokens(holder, extraTokens) {
|
|
65
|
+
if (extraTokens > 0 && holder && typeof holder === "object") {
|
|
66
|
+
const counted = Number(holder.usage?.turnTotalTokens);
|
|
67
|
+
holder.usage = { ...(holder.usage || {}), turnTotalTokens: (Number.isFinite(counted) ? counted : 0) + extraTokens };
|
|
68
|
+
}
|
|
69
|
+
return holder;
|
|
70
|
+
}
|
|
71
|
+
/** A failed or interrupted turn still spent tokens: expose them on the error. */
|
|
72
|
+
function attachTurnUsage(error, usage) {
|
|
73
|
+
if (error && typeof error === "object" && error.usage === undefined) {
|
|
74
|
+
error.usage = usage;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
59
77
|
function normalizeItemId(value) {
|
|
60
78
|
return typeof value === "string" ? value.trim() : "";
|
|
61
79
|
}
|
|
@@ -103,7 +121,7 @@ function resolveItemPhase(item) {
|
|
|
103
121
|
includesCandidate("reason", "summary")) {
|
|
104
122
|
return "reasoning";
|
|
105
123
|
}
|
|
106
|
-
if (hasCandidate("message", "agent_message", "output_text", "final_answer")) {
|
|
124
|
+
if (hasCandidate("message", "agentmessage", "agent_message", "output_text", "final_answer")) {
|
|
107
125
|
return "assistant_message";
|
|
108
126
|
}
|
|
109
127
|
if (hasCandidate("compaction", "context_compacted", "compacted") || includesCandidate("compact")) {
|
|
@@ -185,6 +203,7 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
185
203
|
*/
|
|
186
204
|
static capabilities = Object.freeze({
|
|
187
205
|
goal: true,
|
|
206
|
+
compact: true,
|
|
188
207
|
media: PROVIDER_MEDIA_CAPABILITIES[CODEX_APP_SERVER_VARIANT],
|
|
189
208
|
});
|
|
190
209
|
/**
|
|
@@ -219,6 +238,10 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
219
238
|
this.nativeSessionId = "";
|
|
220
239
|
this.rateLimits = null;
|
|
221
240
|
this.tokenUsage = null;
|
|
241
|
+
// Thread total when the current turn/goal/compaction started, and whether
|
|
242
|
+
// its own usage has arrived since; see snapshotTokenUsage().
|
|
243
|
+
this.turnTokenBaseline = null;
|
|
244
|
+
this.turnTokensSeen = false;
|
|
222
245
|
this.currentTurnStatus = null;
|
|
223
246
|
this.currentTurnActivityAt = 0;
|
|
224
247
|
this.now = typeof options.now === "function" ? options.now : () => Date.now();
|
|
@@ -467,6 +490,29 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
467
490
|
}
|
|
468
491
|
}
|
|
469
492
|
}
|
|
493
|
+
/** Start counting a turn's tokens from the thread total known right now. */
|
|
494
|
+
beginTurnTokenAccounting() {
|
|
495
|
+
this.turnTokenBaseline = readTokenCount(this.tokenUsage?.total);
|
|
496
|
+
this.turnTokensSeen = false;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
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.
|
|
503
|
+
*/
|
|
504
|
+
snapshotTokenUsage() {
|
|
505
|
+
if (!this.tokenUsage) {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
const total = readTokenCount(this.tokenUsage.total);
|
|
509
|
+
return {
|
|
510
|
+
...this.tokenUsage,
|
|
511
|
+
...(total !== null && this.turnTokenBaseline !== null && this.turnTokensSeen
|
|
512
|
+
? { turnTotalTokens: Math.max(0, total - this.turnTokenBaseline) }
|
|
513
|
+
: {}),
|
|
514
|
+
};
|
|
515
|
+
}
|
|
470
516
|
resolveContextUsagePercent() {
|
|
471
517
|
const totalTokens = Number(this.tokenUsage?.total?.totalTokens);
|
|
472
518
|
const contextWindow = Number(this.tokenUsage?.modelContextWindow);
|
|
@@ -527,6 +573,8 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
527
573
|
this.manualResumeReady = false;
|
|
528
574
|
this.sessionInfo = null;
|
|
529
575
|
this.tokenUsage = null;
|
|
576
|
+
this.turnTokenBaseline = null;
|
|
577
|
+
this.turnTokensSeen = false;
|
|
530
578
|
this.booted = false;
|
|
531
579
|
this.bootPromise = null;
|
|
532
580
|
}
|
|
@@ -602,7 +650,7 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
602
650
|
if (!finalizedMessageId || !activeMessageId || activeMessageId === finalizedMessageId) {
|
|
603
651
|
currentTurn.activeAssistantMessageId = "";
|
|
604
652
|
}
|
|
605
|
-
if (!text) {
|
|
653
|
+
if (!text || currentTurn.suppressReply) {
|
|
606
654
|
return false;
|
|
607
655
|
}
|
|
608
656
|
await this.emitAssistantMessage(text);
|
|
@@ -790,8 +838,19 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
790
838
|
if (this.closeRequested) {
|
|
791
839
|
return;
|
|
792
840
|
}
|
|
841
|
+
// Multi-agent mode streams spawned sub-agent threads over this connection.
|
|
842
|
+
// Only our own thread may drive turn/message state: a sub-agent's
|
|
843
|
+
// turn/completed must not end the parent turn and drop its final answer.
|
|
844
|
+
const threadId = typeof params?.threadId === "string" ? params.threadId.trim() : "";
|
|
845
|
+
if (threadId && this.sessionId && threadId !== this.sessionId) {
|
|
846
|
+
this.touchTurnActivity();
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
793
849
|
switch (method) {
|
|
794
850
|
case "thread/started":
|
|
851
|
+
if (params?.thread?.parentThreadId) {
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
795
854
|
this.applyThreadInfo(params, { resumeReady: Boolean(this.resumeSessionId) });
|
|
796
855
|
return;
|
|
797
856
|
case "sessionConfigured":
|
|
@@ -917,17 +976,40 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
917
976
|
}
|
|
918
977
|
return;
|
|
919
978
|
}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
979
|
+
// Notifications from one stdout batch run concurrently. Buffer the
|
|
980
|
+
// delta before yielding so item/completed cannot flush a partial reply.
|
|
981
|
+
await Promise.all([
|
|
982
|
+
this.queueAssistantDelta(delta, { messageId }),
|
|
983
|
+
this.emitWorkingStatus({
|
|
984
|
+
phase: "message_aggregation",
|
|
985
|
+
reply_in_progress: true,
|
|
986
|
+
status_line: statusLineForPhase("message_aggregation"),
|
|
987
|
+
}),
|
|
988
|
+
]);
|
|
926
989
|
return;
|
|
927
990
|
}
|
|
928
|
-
case "thread/tokenUsage/updated":
|
|
929
|
-
|
|
991
|
+
case "thread/tokenUsage/updated": {
|
|
992
|
+
const tokenUsage = params?.tokenUsage && typeof params.tokenUsage === "object" ? { ...params.tokenUsage } : null;
|
|
993
|
+
if (this.currentTurn && tokenUsage) {
|
|
994
|
+
// Codex replays a resumed thread's total right after thread/resume,
|
|
995
|
+
// before this turn's own responses (no or another turn id).
|
|
996
|
+
const turnId = this.currentTurn.turnId;
|
|
997
|
+
const beforeTurn = !turnId || Boolean(params?.turnId && params.turnId !== turnId);
|
|
998
|
+
if (this.turnTokenBaseline === null) {
|
|
999
|
+
// No total was known when the turn started (fresh boot or resume):
|
|
1000
|
+
// a replayed total is the baseline; this turn's first response
|
|
1001
|
+
// 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;
|
|
1005
|
+
}
|
|
1006
|
+
if (!beforeTurn) {
|
|
1007
|
+
this.turnTokensSeen = true;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
this.tokenUsage = tokenUsage;
|
|
930
1011
|
return;
|
|
1012
|
+
}
|
|
931
1013
|
case "account/rateLimits/updated":
|
|
932
1014
|
this.rateLimits = params?.rateLimits && typeof params.rateLimits === "object" ? { ...params.rateLimits } : null;
|
|
933
1015
|
return;
|
|
@@ -1023,7 +1105,7 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
1023
1105
|
await this.emitTurnCompletedStatus(currentTurn);
|
|
1024
1106
|
currentTurn.resolve({
|
|
1025
1107
|
turn,
|
|
1026
|
-
usage: this.
|
|
1108
|
+
usage: this.snapshotTokenUsage(),
|
|
1027
1109
|
});
|
|
1028
1110
|
this.currentTurn = null;
|
|
1029
1111
|
return;
|
|
@@ -1145,6 +1227,7 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
1145
1227
|
resolve: resolveTurn,
|
|
1146
1228
|
reject: rejectTurn,
|
|
1147
1229
|
};
|
|
1230
|
+
this.beginTurnTokenAccounting();
|
|
1148
1231
|
this.currentTurn = currentTurn;
|
|
1149
1232
|
try {
|
|
1150
1233
|
const startPromise = this.transport.request("turn/start", {
|
|
@@ -1203,6 +1286,8 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
1203
1286
|
!this.closeRequested &&
|
|
1204
1287
|
!currentTurn.goalMode &&
|
|
1205
1288
|
this.isRecoverableContextOverflow(error)) {
|
|
1289
|
+
// The overflowed attempt already spent tokens; the fresh thread's totals won't include them.
|
|
1290
|
+
const failedAttemptTokens = this.snapshotTokenUsage()?.turnTotalTokens ?? 0;
|
|
1206
1291
|
// Drop the user message pushed for the failed attempt; the retry
|
|
1207
1292
|
// re-adds it (and folds prior history in as a preface via buildPrompt).
|
|
1208
1293
|
if (this.history.length > 0 &&
|
|
@@ -1227,12 +1312,15 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
1227
1312
|
status_line: "codex context full; retrying on a fresh thread",
|
|
1228
1313
|
});
|
|
1229
1314
|
this.emit("thread_recovered", { reason: "context_overflow", backend: this.backend });
|
|
1230
|
-
return await this.runTurn(promptText, {
|
|
1315
|
+
return addTurnTokens(await this.runTurn(promptText, {
|
|
1231
1316
|
useInitialImages,
|
|
1232
1317
|
media: mediaInput,
|
|
1233
1318
|
contextFiles,
|
|
1234
1319
|
jsonSchema,
|
|
1235
|
-
});
|
|
1320
|
+
}), failedAttemptTokens);
|
|
1321
|
+
}
|
|
1322
|
+
catch (retryError) {
|
|
1323
|
+
throw addTurnTokens(retryError, failedAttemptTokens);
|
|
1236
1324
|
}
|
|
1237
1325
|
finally {
|
|
1238
1326
|
this._contextRecoveryInFlight = false;
|
|
@@ -1246,6 +1334,97 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
1246
1334
|
});
|
|
1247
1335
|
}
|
|
1248
1336
|
this.maybeEmitAuthRequired(error);
|
|
1337
|
+
attachTurnUsage(error, this.snapshotTokenUsage());
|
|
1338
|
+
throw error;
|
|
1339
|
+
}
|
|
1340
|
+
finally {
|
|
1341
|
+
if (this.currentTurn === currentTurn) {
|
|
1342
|
+
this.currentTurn = null;
|
|
1343
|
+
}
|
|
1344
|
+
closeGuard.cleanup();
|
|
1345
|
+
turnTimeoutGuard.cleanup();
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Manually compact the thread via `thread/compact/start`. The app-server
|
|
1350
|
+
* runs compaction as a regular turn, so completion is the usual
|
|
1351
|
+
* `turn/completed`. The protocol takes no focus instructions.
|
|
1352
|
+
*
|
|
1353
|
+
* @param {import("../shared.js").CompactRequest} [request]
|
|
1354
|
+
* @returns {Promise<import("../shared.js").CompactResult>}
|
|
1355
|
+
*/
|
|
1356
|
+
async runCompact(request = {}) {
|
|
1357
|
+
if (this.closeRequested) {
|
|
1358
|
+
throw this.createSessionClosedError();
|
|
1359
|
+
}
|
|
1360
|
+
if (this.currentTurn || this.currentGoalRun) {
|
|
1361
|
+
throw createTurnError("Codex app-server turn already running", {
|
|
1362
|
+
reason: "turn_already_running",
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
const instructions = typeof request?.instructions === "string" ? request.instructions.trim() : "";
|
|
1366
|
+
if (instructions) {
|
|
1367
|
+
this.trace("thread/compact/start takes no instructions; ignoring them");
|
|
1368
|
+
}
|
|
1369
|
+
if (this.pendingHistorySeed || (!this.resumeSessionId && this.history.length === 0)) {
|
|
1370
|
+
return { compact: { status: "noop", instructionsApplied: false }, usage: null, metadata: {} };
|
|
1371
|
+
}
|
|
1372
|
+
this.markTurnStartedStatus();
|
|
1373
|
+
try {
|
|
1374
|
+
await this.boot();
|
|
1375
|
+
}
|
|
1376
|
+
catch (error) {
|
|
1377
|
+
await this.failPendingTurnStart(error);
|
|
1378
|
+
throw error;
|
|
1379
|
+
}
|
|
1380
|
+
const closeGuard = this.createCloseGuard();
|
|
1381
|
+
const turnTimeoutGuard = this.createTurnTimeoutGuard();
|
|
1382
|
+
let resolveTurn = null;
|
|
1383
|
+
let rejectTurn = null;
|
|
1384
|
+
const completion = new Promise((resolve, reject) => {
|
|
1385
|
+
resolveTurn = resolve;
|
|
1386
|
+
rejectTurn = reject;
|
|
1387
|
+
});
|
|
1388
|
+
const currentTurn = {
|
|
1389
|
+
turnId: "",
|
|
1390
|
+
fullText: "",
|
|
1391
|
+
activeAssistantMessageId: "",
|
|
1392
|
+
activeAssistantMessageText: "",
|
|
1393
|
+
resolve: resolveTurn,
|
|
1394
|
+
reject: rejectTurn,
|
|
1395
|
+
suppressReply: true,
|
|
1396
|
+
};
|
|
1397
|
+
this.beginTurnTokenAccounting();
|
|
1398
|
+
this.currentTurn = currentTurn;
|
|
1399
|
+
const metadata = { source: CODEX_APP_SERVER_VARIANT, threadId: this.sessionId };
|
|
1400
|
+
try {
|
|
1401
|
+
const completionResult = await Promise.race([
|
|
1402
|
+
(async () => {
|
|
1403
|
+
await this.transport.request("thread/compact/start", { threadId: this.sessionId });
|
|
1404
|
+
return await completion;
|
|
1405
|
+
})(),
|
|
1406
|
+
closeGuard.promise,
|
|
1407
|
+
turnTimeoutGuard.promise,
|
|
1408
|
+
]);
|
|
1409
|
+
await this.emitTurnCompletedStatus(currentTurn);
|
|
1410
|
+
return {
|
|
1411
|
+
compact: { status: "compacted", instructionsApplied: false },
|
|
1412
|
+
usage: completionResult?.usage || null,
|
|
1413
|
+
metadata,
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
catch (error) {
|
|
1417
|
+
if (error?.reason === "turn_timeout") {
|
|
1418
|
+
await this.interruptCurrentTurn();
|
|
1419
|
+
}
|
|
1420
|
+
if (!this.closeRequested && error?.reason !== "session_closed") {
|
|
1421
|
+
await this.emitWorkingStatus({
|
|
1422
|
+
phase: "turn_failed",
|
|
1423
|
+
reply_in_progress: false,
|
|
1424
|
+
status_done_line: error instanceof Error ? error.message : String(error),
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
attachTurnUsage(error, this.snapshotTokenUsage());
|
|
1249
1428
|
throw error;
|
|
1250
1429
|
}
|
|
1251
1430
|
finally {
|
|
@@ -1368,7 +1547,7 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
1368
1547
|
if (isTerminalGoalStatusLocal(payload.status)) {
|
|
1369
1548
|
resolveGoal?.({
|
|
1370
1549
|
goal: { ...goalRun.latestGoal },
|
|
1371
|
-
usage: this.
|
|
1550
|
+
usage: this.snapshotTokenUsage(),
|
|
1372
1551
|
cleared: false,
|
|
1373
1552
|
});
|
|
1374
1553
|
}
|
|
@@ -1382,7 +1561,7 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
1382
1561
|
};
|
|
1383
1562
|
resolveGoal?.({
|
|
1384
1563
|
goal: { ...finalGoal, status: finalGoal.status || "complete" },
|
|
1385
|
-
usage: this.
|
|
1564
|
+
usage: this.snapshotTokenUsage(),
|
|
1386
1565
|
cleared: true,
|
|
1387
1566
|
});
|
|
1388
1567
|
},
|
|
@@ -1406,6 +1585,7 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
1406
1585
|
},
|
|
1407
1586
|
goalMode: true,
|
|
1408
1587
|
};
|
|
1588
|
+
this.beginTurnTokenAccounting();
|
|
1409
1589
|
this.currentTurn = currentTurn;
|
|
1410
1590
|
try {
|
|
1411
1591
|
let setResult;
|
|
@@ -1440,7 +1620,7 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
1440
1620
|
if (isTerminalGoalStatusLocal(initialGoal.status)) {
|
|
1441
1621
|
resolveGoal?.({
|
|
1442
1622
|
goal: { ...initialGoal },
|
|
1443
|
-
usage: this.
|
|
1623
|
+
usage: this.snapshotTokenUsage(),
|
|
1444
1624
|
cleared: false,
|
|
1445
1625
|
});
|
|
1446
1626
|
}
|
|
@@ -1480,7 +1660,8 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
1480
1660
|
status: goalState.status,
|
|
1481
1661
|
tokenBudget: goalState.tokenBudget !== undefined ? goalState.tokenBudget : tokenBudget,
|
|
1482
1662
|
},
|
|
1483
|
-
|
|
1663
|
+
// Fresh snapshot: usage can still arrive after the terminal goal status.
|
|
1664
|
+
usage: this.snapshotTokenUsage() || goalResult?.usage || null,
|
|
1484
1665
|
metadata: {
|
|
1485
1666
|
source: CODEX_APP_SERVER_VARIANT,
|
|
1486
1667
|
threadId: this.sessionId,
|
|
@@ -1502,6 +1683,7 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
1502
1683
|
});
|
|
1503
1684
|
}
|
|
1504
1685
|
this.maybeEmitAuthRequired(error);
|
|
1686
|
+
attachTurnUsage(error, this.snapshotTokenUsage());
|
|
1505
1687
|
throw error;
|
|
1506
1688
|
}
|
|
1507
1689
|
finally {
|
|
@@ -41,7 +41,7 @@ export class CopilotSdkSession extends EventEmitter<[never]> {
|
|
|
41
41
|
abortRequested: boolean;
|
|
42
42
|
terminalWorkingStatusEmitted: boolean;
|
|
43
43
|
error: null;
|
|
44
|
-
onProgress:
|
|
44
|
+
onProgress: any;
|
|
45
45
|
} | null;
|
|
46
46
|
lastUsage: any;
|
|
47
47
|
currentTurnStatus: any;
|
|
@@ -84,6 +84,7 @@ export class CopilotSdkSession extends EventEmitter<[never]> {
|
|
|
84
84
|
} | null;
|
|
85
85
|
currentTurnStatus: any;
|
|
86
86
|
capabilities: {
|
|
87
|
+
compact: boolean;
|
|
87
88
|
media: Readonly<{
|
|
88
89
|
image: "native";
|
|
89
90
|
}>;
|
|
@@ -170,6 +171,33 @@ export class CopilotSdkSession extends EventEmitter<[never]> {
|
|
|
170
171
|
handleCopilotEventFailure(error: any): void;
|
|
171
172
|
handleCopilotEvent(event: any): Promise<void>;
|
|
172
173
|
interruptCurrentTurn(): Promise<void>;
|
|
174
|
+
createTurnState(onProgress: any): {
|
|
175
|
+
items: never[];
|
|
176
|
+
events: never[];
|
|
177
|
+
fullText: string;
|
|
178
|
+
messageOrder: never[];
|
|
179
|
+
messageText: Map<any, any>;
|
|
180
|
+
emittedMessageIds: Set<any>;
|
|
181
|
+
activeToolName: string;
|
|
182
|
+
activeToolPhase: string;
|
|
183
|
+
abortRequested: boolean;
|
|
184
|
+
terminalWorkingStatusEmitted: boolean;
|
|
185
|
+
error: null;
|
|
186
|
+
onProgress: any;
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* Manually compact the session via the CLI's `session.history.compact` RPC
|
|
190
|
+
* (the same call its interactive `/compact` makes). `customInstructions`
|
|
191
|
+
* is not in the SDK's typed wrapper, so the request goes through the raw
|
|
192
|
+
* connection.
|
|
193
|
+
*
|
|
194
|
+
* @param {import("../shared.js").CompactRequest} [request]
|
|
195
|
+
* @param {{ onProgress?: Function }} [options]
|
|
196
|
+
* @returns {Promise<import("../shared.js").CompactResult>}
|
|
197
|
+
*/
|
|
198
|
+
runCompact(request?: import("../shared.js").CompactRequest, { onProgress }?: {
|
|
199
|
+
onProgress?: Function;
|
|
200
|
+
}): Promise<import("../shared.js").CompactResult>;
|
|
173
201
|
runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, onProgress }?: {
|
|
174
202
|
useInitialImages?: boolean | undefined;
|
|
175
203
|
onProgress?: null | undefined;
|
|
@@ -594,7 +594,7 @@ export class CopilotSdkSession extends EventEmitter {
|
|
|
594
594
|
}
|
|
595
595
|
: null,
|
|
596
596
|
currentTurnStatus: this.getCurrentTurnStatus(),
|
|
597
|
-
capabilities: { media: PROVIDER_MEDIA_CAPABILITIES[COPILOT_PROVIDER_VARIANT] },
|
|
597
|
+
capabilities: { compact: true, media: PROVIDER_MEDIA_CAPABILITIES[COPILOT_PROVIDER_VARIANT] },
|
|
598
598
|
};
|
|
599
599
|
}
|
|
600
600
|
getSessionInfo() {
|
|
@@ -709,7 +709,7 @@ export class CopilotSdkSession extends EventEmitter {
|
|
|
709
709
|
return false;
|
|
710
710
|
}
|
|
711
711
|
const text = currentTurn.messageText.get(messageId) || "";
|
|
712
|
-
if (!text) {
|
|
712
|
+
if (!text || currentTurn.suppressReply) {
|
|
713
713
|
return false;
|
|
714
714
|
}
|
|
715
715
|
currentTurn.emittedMessageIds.add(messageId);
|
|
@@ -1362,6 +1362,11 @@ export class CopilotSdkSession extends EventEmitter {
|
|
|
1362
1362
|
}
|
|
1363
1363
|
}
|
|
1364
1364
|
async interruptCurrentTurn() {
|
|
1365
|
+
if (this.currentTurn?.abortCompaction) {
|
|
1366
|
+
this.currentTurn.abortRequested = true;
|
|
1367
|
+
this.currentTurn.abortCompaction();
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1365
1370
|
if (!this.currentTurn || !this.session || typeof this.session.abort !== "function") {
|
|
1366
1371
|
return;
|
|
1367
1372
|
}
|
|
@@ -1374,6 +1379,109 @@ export class CopilotSdkSession extends EventEmitter {
|
|
|
1374
1379
|
}
|
|
1375
1380
|
}
|
|
1376
1381
|
}
|
|
1382
|
+
createTurnState(onProgress) {
|
|
1383
|
+
return {
|
|
1384
|
+
items: [],
|
|
1385
|
+
events: [],
|
|
1386
|
+
fullText: "",
|
|
1387
|
+
messageOrder: [],
|
|
1388
|
+
messageText: new Map(),
|
|
1389
|
+
emittedMessageIds: new Set(),
|
|
1390
|
+
activeToolName: "",
|
|
1391
|
+
activeToolPhase: "",
|
|
1392
|
+
abortRequested: false,
|
|
1393
|
+
terminalWorkingStatusEmitted: false,
|
|
1394
|
+
error: null,
|
|
1395
|
+
onProgress,
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
/**
|
|
1399
|
+
* Manually compact the session via the CLI's `session.history.compact` RPC
|
|
1400
|
+
* (the same call its interactive `/compact` makes). `customInstructions`
|
|
1401
|
+
* is not in the SDK's typed wrapper, so the request goes through the raw
|
|
1402
|
+
* connection.
|
|
1403
|
+
*
|
|
1404
|
+
* @param {import("../shared.js").CompactRequest} [request]
|
|
1405
|
+
* @param {{ onProgress?: Function }} [options]
|
|
1406
|
+
* @returns {Promise<import("../shared.js").CompactResult>}
|
|
1407
|
+
*/
|
|
1408
|
+
async runCompact(request = {}, { onProgress = null } = {}) {
|
|
1409
|
+
if (this.currentTurn) {
|
|
1410
|
+
throw this.createTurnAlreadyRunningError();
|
|
1411
|
+
}
|
|
1412
|
+
const instructions = typeof request?.instructions === "string" ? request.instructions.trim() : "";
|
|
1413
|
+
const noop = { compact: { status: "noop", instructionsApplied: false }, usage: null, metadata: {} };
|
|
1414
|
+
if (this.pendingHistorySeed) {
|
|
1415
|
+
return noop;
|
|
1416
|
+
}
|
|
1417
|
+
const currentTurn = this.createTurnState(onProgress);
|
|
1418
|
+
currentTurn.suppressReply = true;
|
|
1419
|
+
this.currentTurn = currentTurn;
|
|
1420
|
+
this.touchTurnActivity();
|
|
1421
|
+
this.markTurnStartedStatus();
|
|
1422
|
+
// `session.abort()` does not stop a manual compaction; it has its own abort RPC.
|
|
1423
|
+
const abortCompaction = () => {
|
|
1424
|
+
Promise.resolve()
|
|
1425
|
+
.then(() => this.session?.connection?.sendRequest("session.history.abortManualCompaction", {
|
|
1426
|
+
sessionId: this.session.sessionId,
|
|
1427
|
+
}))
|
|
1428
|
+
.catch(() => { });
|
|
1429
|
+
};
|
|
1430
|
+
currentTurn.abortCompaction = abortCompaction;
|
|
1431
|
+
const closeGuard = this.createCloseGuard(abortCompaction);
|
|
1432
|
+
const turnTimeoutGuard = this.createTurnTimeoutGuard(abortCompaction);
|
|
1433
|
+
try {
|
|
1434
|
+
await Promise.race([this.boot(), closeGuard.promise, turnTimeoutGuard.promise]);
|
|
1435
|
+
this.applySessionInfo(this.session?.sessionId || this.sessionId);
|
|
1436
|
+
await this.emitWorkingStatus({ phase: "context_compaction", reply_in_progress: true, status_line: "copilot compacting context" }, onProgress);
|
|
1437
|
+
if (currentTurn.abortRequested) {
|
|
1438
|
+
throw createTurnError("Copilot compaction interrupted", { reason: "turn_interrupted" });
|
|
1439
|
+
}
|
|
1440
|
+
const result = await Promise.race([
|
|
1441
|
+
this.requestOrThrow(this.session.connection.sendRequest("session.history.compact", {
|
|
1442
|
+
sessionId: this.session.sessionId,
|
|
1443
|
+
// The CLI schema caps focus instructions at 4000 characters.
|
|
1444
|
+
...(instructions ? { customInstructions: instructions.slice(0, 4000) } : {}),
|
|
1445
|
+
})),
|
|
1446
|
+
closeGuard.promise,
|
|
1447
|
+
turnTimeoutGuard.promise,
|
|
1448
|
+
]);
|
|
1449
|
+
if (result?.success === false) {
|
|
1450
|
+
throw createTurnError("Copilot compaction failed", { reason: "compact_failed" });
|
|
1451
|
+
}
|
|
1452
|
+
await this.emitTerminalWorkingStatus(currentTurn, { phase: "turn_completed", status_done_line: "copilot compacted context" }, onProgress);
|
|
1453
|
+
const tokensRemoved = Number(result?.tokensRemoved);
|
|
1454
|
+
return {
|
|
1455
|
+
compact: {
|
|
1456
|
+
status: "compacted",
|
|
1457
|
+
instructionsApplied: Boolean(instructions),
|
|
1458
|
+
tokensRemoved: Number.isFinite(tokensRemoved) ? tokensRemoved : undefined,
|
|
1459
|
+
},
|
|
1460
|
+
usage: null,
|
|
1461
|
+
metadata: { source: COPILOT_PROVIDER_VARIANT, sessionId: this.sessionId || undefined },
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
catch (error) {
|
|
1465
|
+
const message = extractErrorMessage(error);
|
|
1466
|
+
// The CLI refuses when the session holds nothing it can summarize yet.
|
|
1467
|
+
if (!currentTurn.abortRequested && /nothing to compact|no active agent context/i.test(message)) {
|
|
1468
|
+
await this.emitTerminalWorkingStatus(currentTurn, { phase: "turn_completed", status_done_line: "copilot finished" }, onProgress);
|
|
1469
|
+
return noop;
|
|
1470
|
+
}
|
|
1471
|
+
if (currentTurn.abortRequested && error?.reason !== "turn_timeout" && error?.reason !== "session_closed") {
|
|
1472
|
+
await this.emitTerminalWorkingStatus(currentTurn, { phase: "turn_interrupted", status_done_line: "copilot interrupted" }, onProgress);
|
|
1473
|
+
throw createTurnError("Copilot compaction interrupted", { reason: "turn_interrupted" });
|
|
1474
|
+
}
|
|
1475
|
+
await this.emitTerminalWorkingStatus(currentTurn, { phase: "turn_failed", status_done_line: message }, onProgress);
|
|
1476
|
+
throw error;
|
|
1477
|
+
}
|
|
1478
|
+
finally {
|
|
1479
|
+
closeGuard.cleanup();
|
|
1480
|
+
turnTimeoutGuard.cleanup();
|
|
1481
|
+
this.activeReplyTarget = "";
|
|
1482
|
+
this.currentTurn = null;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1377
1485
|
async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null } = {}) {
|
|
1378
1486
|
if (this.currentTurn) {
|
|
1379
1487
|
throw this.createTurnAlreadyRunningError();
|
|
@@ -1394,20 +1502,7 @@ export class CopilotSdkSession extends EventEmitter {
|
|
|
1394
1502
|
},
|
|
1395
1503
|
};
|
|
1396
1504
|
}
|
|
1397
|
-
const currentTurn =
|
|
1398
|
-
items: [],
|
|
1399
|
-
events: [],
|
|
1400
|
-
fullText: "",
|
|
1401
|
-
messageOrder: [],
|
|
1402
|
-
messageText: new Map(),
|
|
1403
|
-
emittedMessageIds: new Set(),
|
|
1404
|
-
activeToolName: "",
|
|
1405
|
-
activeToolPhase: "",
|
|
1406
|
-
abortRequested: false,
|
|
1407
|
-
terminalWorkingStatusEmitted: false,
|
|
1408
|
-
error: null,
|
|
1409
|
-
onProgress,
|
|
1410
|
-
};
|
|
1505
|
+
const currentTurn = this.createTurnState(onProgress);
|
|
1411
1506
|
this.currentTurn = currentTurn;
|
|
1412
1507
|
this.touchTurnActivity();
|
|
1413
1508
|
this.markTurnStartedStatus();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export function defaultDshSessionRoot(): string;
|
|
2
|
+
export const COMPACT_SUMMARY_PROMPT: string;
|
|
2
3
|
/**
|
|
3
4
|
* DeepSeek Harness (dsh) backend session.
|
|
4
5
|
*
|
|
@@ -17,10 +18,12 @@ export function defaultDshSessionRoot(): string;
|
|
|
17
18
|
export class DshSdkSession extends EventEmitter<[never]> {
|
|
18
19
|
static capabilities: Readonly<{
|
|
19
20
|
goal: false;
|
|
21
|
+
compact: true;
|
|
20
22
|
}>;
|
|
21
23
|
constructor(backend: any, options?: {});
|
|
22
24
|
getCapabilities(): {
|
|
23
25
|
goal: false;
|
|
26
|
+
compact: true;
|
|
24
27
|
};
|
|
25
28
|
backend: string;
|
|
26
29
|
options: {};
|
|
@@ -52,6 +55,7 @@ export class DshSdkSession extends EventEmitter<[never]> {
|
|
|
52
55
|
manualResumeReady: boolean;
|
|
53
56
|
sessionAnnounced: boolean;
|
|
54
57
|
currentTurn: {
|
|
58
|
+
suppressReply: boolean;
|
|
55
59
|
emittedAssistantMessage: boolean;
|
|
56
60
|
fullText: string;
|
|
57
61
|
interrupted: boolean;
|
|
@@ -87,6 +91,7 @@ export class DshSdkSession extends EventEmitter<[never]> {
|
|
|
87
91
|
currentTurnStatus: any;
|
|
88
92
|
capabilities: {
|
|
89
93
|
goal: false;
|
|
94
|
+
compact: true;
|
|
90
95
|
};
|
|
91
96
|
};
|
|
92
97
|
getSessionInfo(): {
|
|
@@ -165,9 +170,23 @@ export class DshSdkSession extends EventEmitter<[never]> {
|
|
|
165
170
|
onProgress: any;
|
|
166
171
|
}): Promise<void>;
|
|
167
172
|
interruptCurrentTurn(): Promise<boolean>;
|
|
168
|
-
|
|
173
|
+
/**
|
|
174
|
+
* Manually compact the conversation. The dsh SDK wire exposes no compaction
|
|
175
|
+
* request (its engine's `compactNow` is internal), so the model summarizes
|
|
176
|
+
* the conversation in one silent turn and the chat continues on a fresh
|
|
177
|
+
* wire session seeded with only that summary.
|
|
178
|
+
*
|
|
179
|
+
* @param {import("../shared.js").CompactRequest} [request]
|
|
180
|
+
* @param {{ onProgress?: Function }} [options]
|
|
181
|
+
* @returns {Promise<import("../shared.js").CompactResult>}
|
|
182
|
+
*/
|
|
183
|
+
runCompact(request?: import("../shared.js").CompactRequest, { onProgress }?: {
|
|
184
|
+
onProgress?: Function;
|
|
185
|
+
}): Promise<import("../shared.js").CompactResult>;
|
|
186
|
+
runTurn(promptText: any, { useInitialImages, onProgress, suppressReply }?: {
|
|
169
187
|
useInitialImages?: boolean | undefined;
|
|
170
188
|
onProgress?: null | undefined;
|
|
189
|
+
suppressReply?: boolean | undefined;
|
|
171
190
|
}): Promise<{
|
|
172
191
|
text: string;
|
|
173
192
|
usage: null;
|