@love-moon/ai-sdk 0.13.0 → 0.13.2
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 +35 -0
- package/dist/client.d.ts +10 -0
- package/dist/client.js +38 -1
- package/dist/providers/claude-agent-sdk-session.d.ts +25 -1
- package/dist/providers/claude-agent-sdk-session.js +114 -11
- package/dist/providers/codex-app-server-session.d.ts +44 -0
- package/dist/providers/codex-app-server-session.js +182 -12
- package/dist/providers/codex-exec-session.js +9 -2
- package/dist/providers/copilot-sdk-session.d.ts +29 -1
- package/dist/providers/copilot-sdk-session.js +115 -18
- package/dist/providers/dsh-sdk-session.d.ts +20 -1
- package/dist/providers/dsh-sdk-session.js +81 -10
- package/dist/providers/kimi-cli-session.d.ts +18 -1
- package/dist/providers/kimi-cli-session.js +40 -7
- package/dist/providers/kimi-print-session.d.ts +16 -1
- package/dist/providers/kimi-print-session.js +51 -6
- package/dist/providers/opencode-sdk-session.d.ts +44 -2
- package/dist/providers/opencode-sdk-session.js +177 -26
- package/dist/shared.d.ts +78 -0
- package/dist/shared.js +95 -1
- package/dist/worker.js +1 -1
- package/package.json +3 -3
|
@@ -4,7 +4,7 @@ import { appendContextFilesToPrompt } from "../context-files.js";
|
|
|
4
4
|
import { PROVIDER_MEDIA_CAPABILITIES, buildCodexAppServerInput } from "../media-adapters.js";
|
|
5
5
|
import { assertMediaCapabilities, defaultPromptForMedia, resolveTurnMedia, } from "../media-input.js";
|
|
6
6
|
import { CodexAppServerTransport } from "../transports/codex-app-server-transport.js";
|
|
7
|
-
import { TERMINAL_GOAL_STATUSES, emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, proxyToEnv, sanitizeForLog, } from "../shared.js";
|
|
7
|
+
import { TERMINAL_GOAL_STATUSES, describeCodexToolItem, emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, noteToolFinished, noteToolStarted, proxyToEnv, sanitizeForLog, withActiveTool, } from "../shared.js";
|
|
8
8
|
const TERMINAL_GOAL_STATUSES_LOCAL = new Set(TERMINAL_GOAL_STATUSES);
|
|
9
9
|
function isTerminalGoalStatusLocal(status) {
|
|
10
10
|
return typeof status === "string" && TERMINAL_GOAL_STATUSES_LOCAL.has(status);
|
|
@@ -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
|
}
|
|
@@ -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();
|
|
@@ -301,7 +324,7 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
301
324
|
return this.sessionInfo ? { ...this.sessionInfo } : null;
|
|
302
325
|
}
|
|
303
326
|
getCurrentTurnStatus() {
|
|
304
|
-
return this.currentTurnStatus
|
|
327
|
+
return withActiveTool(this.currentTurnStatus, this.currentTurn);
|
|
305
328
|
}
|
|
306
329
|
async ensureSessionInfo() {
|
|
307
330
|
await this.boot();
|
|
@@ -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);
|
|
@@ -835,6 +883,8 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
835
883
|
if (!currentTurn) {
|
|
836
884
|
return;
|
|
837
885
|
}
|
|
886
|
+
const tool = describeCodexToolItem(params?.item);
|
|
887
|
+
noteToolStarted(currentTurn, extractItemId(params?.item), tool?.name, tool?.input);
|
|
838
888
|
const phase = resolveItemPhase(params?.item);
|
|
839
889
|
if (!phase) {
|
|
840
890
|
return;
|
|
@@ -864,6 +914,7 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
864
914
|
if (!currentTurn) {
|
|
865
915
|
return;
|
|
866
916
|
}
|
|
917
|
+
noteToolFinished(currentTurn, extractItemId(params?.item));
|
|
867
918
|
const phase = resolveItemPhase(params?.item);
|
|
868
919
|
if (phase !== "assistant_message") {
|
|
869
920
|
return;
|
|
@@ -922,9 +973,28 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
922
973
|
await this.queueAssistantDelta(delta, { messageId });
|
|
923
974
|
return;
|
|
924
975
|
}
|
|
925
|
-
case "thread/tokenUsage/updated":
|
|
926
|
-
|
|
976
|
+
case "thread/tokenUsage/updated": {
|
|
977
|
+
const tokenUsage = params?.tokenUsage && typeof params.tokenUsage === "object" ? { ...params.tokenUsage } : null;
|
|
978
|
+
if (this.currentTurn && tokenUsage) {
|
|
979
|
+
// Codex replays a resumed thread's total right after thread/resume,
|
|
980
|
+
// before this turn's own responses (no or another turn id).
|
|
981
|
+
const turnId = this.currentTurn.turnId;
|
|
982
|
+
const beforeTurn = !turnId || Boolean(params?.turnId && params.turnId !== turnId);
|
|
983
|
+
if (this.turnTokenBaseline === null) {
|
|
984
|
+
// No total was known when the turn started (fresh boot or resume):
|
|
985
|
+
// a replayed total is the baseline; this turn's first response
|
|
986
|
+
// has `total - last` before it.
|
|
987
|
+
const total = readTokenCount(tokenUsage.total);
|
|
988
|
+
const last = readTokenCount(tokenUsage.last);
|
|
989
|
+
this.turnTokenBaseline = total === null ? null : beforeTurn ? total : last === null ? null : total - last;
|
|
990
|
+
}
|
|
991
|
+
if (!beforeTurn) {
|
|
992
|
+
this.turnTokensSeen = true;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
this.tokenUsage = tokenUsage;
|
|
927
996
|
return;
|
|
997
|
+
}
|
|
928
998
|
case "account/rateLimits/updated":
|
|
929
999
|
this.rateLimits = params?.rateLimits && typeof params.rateLimits === "object" ? { ...params.rateLimits } : null;
|
|
930
1000
|
return;
|
|
@@ -1020,7 +1090,7 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
1020
1090
|
await this.emitTurnCompletedStatus(currentTurn);
|
|
1021
1091
|
currentTurn.resolve({
|
|
1022
1092
|
turn,
|
|
1023
|
-
usage: this.
|
|
1093
|
+
usage: this.snapshotTokenUsage(),
|
|
1024
1094
|
});
|
|
1025
1095
|
this.currentTurn = null;
|
|
1026
1096
|
return;
|
|
@@ -1142,6 +1212,7 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
1142
1212
|
resolve: resolveTurn,
|
|
1143
1213
|
reject: rejectTurn,
|
|
1144
1214
|
};
|
|
1215
|
+
this.beginTurnTokenAccounting();
|
|
1145
1216
|
this.currentTurn = currentTurn;
|
|
1146
1217
|
try {
|
|
1147
1218
|
const startPromise = this.transport.request("turn/start", {
|
|
@@ -1200,6 +1271,8 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
1200
1271
|
!this.closeRequested &&
|
|
1201
1272
|
!currentTurn.goalMode &&
|
|
1202
1273
|
this.isRecoverableContextOverflow(error)) {
|
|
1274
|
+
// The overflowed attempt already spent tokens; the fresh thread's totals won't include them.
|
|
1275
|
+
const failedAttemptTokens = this.snapshotTokenUsage()?.turnTotalTokens ?? 0;
|
|
1203
1276
|
// Drop the user message pushed for the failed attempt; the retry
|
|
1204
1277
|
// re-adds it (and folds prior history in as a preface via buildPrompt).
|
|
1205
1278
|
if (this.history.length > 0 &&
|
|
@@ -1224,12 +1297,15 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
1224
1297
|
status_line: "codex context full; retrying on a fresh thread",
|
|
1225
1298
|
});
|
|
1226
1299
|
this.emit("thread_recovered", { reason: "context_overflow", backend: this.backend });
|
|
1227
|
-
return await this.runTurn(promptText, {
|
|
1300
|
+
return addTurnTokens(await this.runTurn(promptText, {
|
|
1228
1301
|
useInitialImages,
|
|
1229
1302
|
media: mediaInput,
|
|
1230
1303
|
contextFiles,
|
|
1231
1304
|
jsonSchema,
|
|
1232
|
-
});
|
|
1305
|
+
}), failedAttemptTokens);
|
|
1306
|
+
}
|
|
1307
|
+
catch (retryError) {
|
|
1308
|
+
throw addTurnTokens(retryError, failedAttemptTokens);
|
|
1233
1309
|
}
|
|
1234
1310
|
finally {
|
|
1235
1311
|
this._contextRecoveryInFlight = false;
|
|
@@ -1243,6 +1319,97 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
1243
1319
|
});
|
|
1244
1320
|
}
|
|
1245
1321
|
this.maybeEmitAuthRequired(error);
|
|
1322
|
+
attachTurnUsage(error, this.snapshotTokenUsage());
|
|
1323
|
+
throw error;
|
|
1324
|
+
}
|
|
1325
|
+
finally {
|
|
1326
|
+
if (this.currentTurn === currentTurn) {
|
|
1327
|
+
this.currentTurn = null;
|
|
1328
|
+
}
|
|
1329
|
+
closeGuard.cleanup();
|
|
1330
|
+
turnTimeoutGuard.cleanup();
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Manually compact the thread via `thread/compact/start`. The app-server
|
|
1335
|
+
* runs compaction as a regular turn, so completion is the usual
|
|
1336
|
+
* `turn/completed`. The protocol takes no focus instructions.
|
|
1337
|
+
*
|
|
1338
|
+
* @param {import("../shared.js").CompactRequest} [request]
|
|
1339
|
+
* @returns {Promise<import("../shared.js").CompactResult>}
|
|
1340
|
+
*/
|
|
1341
|
+
async runCompact(request = {}) {
|
|
1342
|
+
if (this.closeRequested) {
|
|
1343
|
+
throw this.createSessionClosedError();
|
|
1344
|
+
}
|
|
1345
|
+
if (this.currentTurn || this.currentGoalRun) {
|
|
1346
|
+
throw createTurnError("Codex app-server turn already running", {
|
|
1347
|
+
reason: "turn_already_running",
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
const instructions = typeof request?.instructions === "string" ? request.instructions.trim() : "";
|
|
1351
|
+
if (instructions) {
|
|
1352
|
+
this.trace("thread/compact/start takes no instructions; ignoring them");
|
|
1353
|
+
}
|
|
1354
|
+
if (this.pendingHistorySeed || (!this.resumeSessionId && this.history.length === 0)) {
|
|
1355
|
+
return { compact: { status: "noop", instructionsApplied: false }, usage: null, metadata: {} };
|
|
1356
|
+
}
|
|
1357
|
+
this.markTurnStartedStatus();
|
|
1358
|
+
try {
|
|
1359
|
+
await this.boot();
|
|
1360
|
+
}
|
|
1361
|
+
catch (error) {
|
|
1362
|
+
await this.failPendingTurnStart(error);
|
|
1363
|
+
throw error;
|
|
1364
|
+
}
|
|
1365
|
+
const closeGuard = this.createCloseGuard();
|
|
1366
|
+
const turnTimeoutGuard = this.createTurnTimeoutGuard();
|
|
1367
|
+
let resolveTurn = null;
|
|
1368
|
+
let rejectTurn = null;
|
|
1369
|
+
const completion = new Promise((resolve, reject) => {
|
|
1370
|
+
resolveTurn = resolve;
|
|
1371
|
+
rejectTurn = reject;
|
|
1372
|
+
});
|
|
1373
|
+
const currentTurn = {
|
|
1374
|
+
turnId: "",
|
|
1375
|
+
fullText: "",
|
|
1376
|
+
activeAssistantMessageId: "",
|
|
1377
|
+
activeAssistantMessageText: "",
|
|
1378
|
+
resolve: resolveTurn,
|
|
1379
|
+
reject: rejectTurn,
|
|
1380
|
+
suppressReply: true,
|
|
1381
|
+
};
|
|
1382
|
+
this.beginTurnTokenAccounting();
|
|
1383
|
+
this.currentTurn = currentTurn;
|
|
1384
|
+
const metadata = { source: CODEX_APP_SERVER_VARIANT, threadId: this.sessionId };
|
|
1385
|
+
try {
|
|
1386
|
+
const completionResult = await Promise.race([
|
|
1387
|
+
(async () => {
|
|
1388
|
+
await this.transport.request("thread/compact/start", { threadId: this.sessionId });
|
|
1389
|
+
return await completion;
|
|
1390
|
+
})(),
|
|
1391
|
+
closeGuard.promise,
|
|
1392
|
+
turnTimeoutGuard.promise,
|
|
1393
|
+
]);
|
|
1394
|
+
await this.emitTurnCompletedStatus(currentTurn);
|
|
1395
|
+
return {
|
|
1396
|
+
compact: { status: "compacted", instructionsApplied: false },
|
|
1397
|
+
usage: completionResult?.usage || null,
|
|
1398
|
+
metadata,
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
catch (error) {
|
|
1402
|
+
if (error?.reason === "turn_timeout") {
|
|
1403
|
+
await this.interruptCurrentTurn();
|
|
1404
|
+
}
|
|
1405
|
+
if (!this.closeRequested && error?.reason !== "session_closed") {
|
|
1406
|
+
await this.emitWorkingStatus({
|
|
1407
|
+
phase: "turn_failed",
|
|
1408
|
+
reply_in_progress: false,
|
|
1409
|
+
status_done_line: error instanceof Error ? error.message : String(error),
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
attachTurnUsage(error, this.snapshotTokenUsage());
|
|
1246
1413
|
throw error;
|
|
1247
1414
|
}
|
|
1248
1415
|
finally {
|
|
@@ -1365,7 +1532,7 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
1365
1532
|
if (isTerminalGoalStatusLocal(payload.status)) {
|
|
1366
1533
|
resolveGoal?.({
|
|
1367
1534
|
goal: { ...goalRun.latestGoal },
|
|
1368
|
-
usage: this.
|
|
1535
|
+
usage: this.snapshotTokenUsage(),
|
|
1369
1536
|
cleared: false,
|
|
1370
1537
|
});
|
|
1371
1538
|
}
|
|
@@ -1379,7 +1546,7 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
1379
1546
|
};
|
|
1380
1547
|
resolveGoal?.({
|
|
1381
1548
|
goal: { ...finalGoal, status: finalGoal.status || "complete" },
|
|
1382
|
-
usage: this.
|
|
1549
|
+
usage: this.snapshotTokenUsage(),
|
|
1383
1550
|
cleared: true,
|
|
1384
1551
|
});
|
|
1385
1552
|
},
|
|
@@ -1403,6 +1570,7 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
1403
1570
|
},
|
|
1404
1571
|
goalMode: true,
|
|
1405
1572
|
};
|
|
1573
|
+
this.beginTurnTokenAccounting();
|
|
1406
1574
|
this.currentTurn = currentTurn;
|
|
1407
1575
|
try {
|
|
1408
1576
|
let setResult;
|
|
@@ -1437,7 +1605,7 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
1437
1605
|
if (isTerminalGoalStatusLocal(initialGoal.status)) {
|
|
1438
1606
|
resolveGoal?.({
|
|
1439
1607
|
goal: { ...initialGoal },
|
|
1440
|
-
usage: this.
|
|
1608
|
+
usage: this.snapshotTokenUsage(),
|
|
1441
1609
|
cleared: false,
|
|
1442
1610
|
});
|
|
1443
1611
|
}
|
|
@@ -1477,7 +1645,8 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
1477
1645
|
status: goalState.status,
|
|
1478
1646
|
tokenBudget: goalState.tokenBudget !== undefined ? goalState.tokenBudget : tokenBudget,
|
|
1479
1647
|
},
|
|
1480
|
-
|
|
1648
|
+
// Fresh snapshot: usage can still arrive after the terminal goal status.
|
|
1649
|
+
usage: this.snapshotTokenUsage() || goalResult?.usage || null,
|
|
1481
1650
|
metadata: {
|
|
1482
1651
|
source: CODEX_APP_SERVER_VARIANT,
|
|
1483
1652
|
threadId: this.sessionId,
|
|
@@ -1499,6 +1668,7 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
1499
1668
|
});
|
|
1500
1669
|
}
|
|
1501
1670
|
this.maybeEmitAuthRequired(error);
|
|
1671
|
+
attachTurnUsage(error, this.snapshotTokenUsage());
|
|
1502
1672
|
throw error;
|
|
1503
1673
|
}
|
|
1504
1674
|
finally {
|
|
@@ -9,7 +9,7 @@ import { CODEX_EXEC_VARIANT as CODEX_EXEC_PROVIDER_VARIANT } from "../built-in-b
|
|
|
9
9
|
import { appendContextFilesToPrompt } from "../context-files.js";
|
|
10
10
|
import { PROVIDER_MEDIA_CAPABILITIES } from "../media-adapters.js";
|
|
11
11
|
import { assertMediaCapabilities, defaultPromptForMedia, resolveTurnMedia, } from "../media-input.js";
|
|
12
|
-
import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, parseCommandParts, proxyToEnv, sanitizeForLog, } from "../shared.js";
|
|
12
|
+
import { describeCodexToolItem, emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, noteToolFinished, noteToolStarted, parseCommandParts, proxyToEnv, sanitizeForLog, withActiveTool, } from "../shared.js";
|
|
13
13
|
const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
|
|
14
14
|
const MIN_TURN_DEADLINE_MS = 30 * 1000;
|
|
15
15
|
const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
|
|
@@ -250,7 +250,7 @@ export class CodexExecSession extends EventEmitter {
|
|
|
250
250
|
return this.sessionInfo ? { ...this.sessionInfo } : null;
|
|
251
251
|
}
|
|
252
252
|
getCurrentTurnStatus() {
|
|
253
|
-
return this.currentTurnStatus
|
|
253
|
+
return withActiveTool(this.currentTurnStatus, this.currentTurn);
|
|
254
254
|
}
|
|
255
255
|
async ensureSessionInfo() {
|
|
256
256
|
this.announceSession();
|
|
@@ -475,6 +475,13 @@ export class CodexExecSession extends EventEmitter {
|
|
|
475
475
|
payload = { type: "raw", line: normalizedLine };
|
|
476
476
|
}
|
|
477
477
|
currentTurn.stdoutEvents.push(payload);
|
|
478
|
+
if (payload?.type === "item.started") {
|
|
479
|
+
const tool = describeCodexToolItem(payload.item);
|
|
480
|
+
noteToolStarted(currentTurn, payload.item?.id, tool?.name, tool?.input);
|
|
481
|
+
}
|
|
482
|
+
else if (payload?.type === "item.completed") {
|
|
483
|
+
noteToolFinished(currentTurn, payload.item?.id);
|
|
484
|
+
}
|
|
478
485
|
const phase = resolveExecPhase(payload);
|
|
479
486
|
if (phase) {
|
|
480
487
|
void this.emitWorkingStatus({
|
|
@@ -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;
|
|
@@ -6,7 +6,7 @@ import { COPILOT_SDK_VARIANT as COPILOT_PROVIDER_VARIANT } from "../built-in-bac
|
|
|
6
6
|
import { appendContextFilesToPrompt } from "../context-files.js";
|
|
7
7
|
import { PROVIDER_MEDIA_CAPABILITIES, buildCopilotAttachments } from "../media-adapters.js";
|
|
8
8
|
import { assertMediaCapabilities, defaultPromptForMedia, resolveTurnMedia, } from "../media-input.js";
|
|
9
|
-
import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, parseCommandParts, proxyToEnv, sanitizeForLog, withoutCopilotGithubTokenEnv, } from "../shared.js";
|
|
9
|
+
import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, noteToolFinished, noteToolStarted, parseCommandParts, proxyToEnv, sanitizeForLog, withoutCopilotGithubTokenEnv, withActiveTool, } from "../shared.js";
|
|
10
10
|
const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
|
|
11
11
|
const MIN_TURN_DEADLINE_MS = 30 * 1000;
|
|
12
12
|
const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
|
|
@@ -594,14 +594,14 @@ 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() {
|
|
601
601
|
return this.sessionInfo ? { ...this.sessionInfo } : null;
|
|
602
602
|
}
|
|
603
603
|
getCurrentTurnStatus() {
|
|
604
|
-
return this.currentTurnStatus
|
|
604
|
+
return withActiveTool(this.currentTurnStatus, this.currentTurn);
|
|
605
605
|
}
|
|
606
606
|
async ensureSessionInfo() {
|
|
607
607
|
await this.boot();
|
|
@@ -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);
|
|
@@ -1199,6 +1199,7 @@ export class CopilotSdkSession extends EventEmitter {
|
|
|
1199
1199
|
}
|
|
1200
1200
|
currentTurn.activeToolName = normalizeText(event.data?.toolName);
|
|
1201
1201
|
currentTurn.activeToolPhase = toolPhaseForName(currentTurn.activeToolName);
|
|
1202
|
+
noteToolStarted(currentTurn, event.data?.toolCallId, currentTurn.activeToolName, event.data?.arguments);
|
|
1202
1203
|
await this.emitWorkingStatus({
|
|
1203
1204
|
phase: currentTurn.activeToolPhase,
|
|
1204
1205
|
reply_in_progress: true,
|
|
@@ -1239,6 +1240,7 @@ export class CopilotSdkSession extends EventEmitter {
|
|
|
1239
1240
|
}
|
|
1240
1241
|
currentTurn.activeToolName = "";
|
|
1241
1242
|
currentTurn.activeToolPhase = "";
|
|
1243
|
+
noteToolFinished(currentTurn, event.data?.toolCallId);
|
|
1242
1244
|
const preview = extractToolResultPreview(event.data?.result);
|
|
1243
1245
|
await this.emitWorkingStatus({
|
|
1244
1246
|
phase: "message_aggregation",
|
|
@@ -1360,6 +1362,11 @@ export class CopilotSdkSession extends EventEmitter {
|
|
|
1360
1362
|
}
|
|
1361
1363
|
}
|
|
1362
1364
|
async interruptCurrentTurn() {
|
|
1365
|
+
if (this.currentTurn?.abortCompaction) {
|
|
1366
|
+
this.currentTurn.abortRequested = true;
|
|
1367
|
+
this.currentTurn.abortCompaction();
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1363
1370
|
if (!this.currentTurn || !this.session || typeof this.session.abort !== "function") {
|
|
1364
1371
|
return;
|
|
1365
1372
|
}
|
|
@@ -1372,6 +1379,109 @@ export class CopilotSdkSession extends EventEmitter {
|
|
|
1372
1379
|
}
|
|
1373
1380
|
}
|
|
1374
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
|
+
}
|
|
1375
1485
|
async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null } = {}) {
|
|
1376
1486
|
if (this.currentTurn) {
|
|
1377
1487
|
throw this.createTurnAlreadyRunningError();
|
|
@@ -1392,20 +1502,7 @@ export class CopilotSdkSession extends EventEmitter {
|
|
|
1392
1502
|
},
|
|
1393
1503
|
};
|
|
1394
1504
|
}
|
|
1395
|
-
const currentTurn =
|
|
1396
|
-
items: [],
|
|
1397
|
-
events: [],
|
|
1398
|
-
fullText: "",
|
|
1399
|
-
messageOrder: [],
|
|
1400
|
-
messageText: new Map(),
|
|
1401
|
-
emittedMessageIds: new Set(),
|
|
1402
|
-
activeToolName: "",
|
|
1403
|
-
activeToolPhase: "",
|
|
1404
|
-
abortRequested: false,
|
|
1405
|
-
terminalWorkingStatusEmitted: false,
|
|
1406
|
-
error: null,
|
|
1407
|
-
onProgress,
|
|
1408
|
-
};
|
|
1505
|
+
const currentTurn = this.createTurnState(onProgress);
|
|
1409
1506
|
this.currentTurn = currentTurn;
|
|
1410
1507
|
this.touchTurnActivity();
|
|
1411
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;
|