@royalcat/opencode-dcp-rc 4.0.0 → 4.0.1
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/dist/index.js +539 -21
- package/dist/index.js.map +1 -1
- package/dist/lib/commands/stats.d.ts +3 -1
- package/dist/lib/commands/stats.d.ts.map +1 -1
- package/dist/lib/compress/rc.d.ts.map +1 -1
- package/dist/lib/compress/summary.d.ts +11 -1
- package/dist/lib/compress/summary.d.ts.map +1 -1
- package/dist/lib/compress/types.d.ts +10 -1
- package/dist/lib/compress/types.d.ts.map +1 -1
- package/dist/lib/compress/usage.d.ts +45 -0
- package/dist/lib/compress/usage.d.ts.map +1 -0
- package/dist/lib/state/persistence.d.ts +2 -0
- package/dist/lib/state/persistence.d.ts.map +1 -1
- package/dist/lib/state/state.d.ts.map +1 -1
- package/dist/lib/state/types.d.ts +3 -0
- package/dist/lib/state/types.d.ts.map +1 -1
- package/dist/lib/tui/dialogs.d.ts.map +1 -1
- package/dist/lib/v2/index.d.ts.map +1 -1
- package/dist/lib/v2/rpc.d.ts +20 -0
- package/dist/lib/v2/rpc.d.ts.map +1 -1
- package/dist/lib/v2/usage.d.ts +44 -0
- package/dist/lib/v2/usage.d.ts.map +1 -0
- package/lib/commands/stats.ts +21 -0
- package/lib/compress/rc.ts +43 -7
- package/lib/compress/summary.ts +28 -1
- package/lib/compress/types.ts +14 -1
- package/lib/compress/usage.ts +168 -0
- package/lib/state/persistence.ts +18 -2
- package/lib/state/state.ts +4 -0
- package/lib/state/types.ts +3 -0
- package/lib/tui/dialogs.tsx +24 -0
- package/lib/v2/index.ts +52 -5
- package/lib/v2/rpc.ts +12 -0
- package/lib/v2/usage.ts +423 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2250,6 +2250,102 @@ import { existsSync as existsSync4 } from "fs";
|
|
|
2250
2250
|
import { homedir as homedir4 } from "os";
|
|
2251
2251
|
import { join as join4 } from "path";
|
|
2252
2252
|
|
|
2253
|
+
// lib/compress/usage.ts
|
|
2254
|
+
function emptyCompressionUsage() {
|
|
2255
|
+
return {
|
|
2256
|
+
calls: 0,
|
|
2257
|
+
providerCalls: 0,
|
|
2258
|
+
estimatedCalls: 0,
|
|
2259
|
+
inputTokens: 0,
|
|
2260
|
+
outputTokens: 0,
|
|
2261
|
+
cacheReadTokens: 0,
|
|
2262
|
+
cacheWriteTokens: 0,
|
|
2263
|
+
reasoningTokens: 0
|
|
2264
|
+
};
|
|
2265
|
+
}
|
|
2266
|
+
function toCount(value, fallback = 0) {
|
|
2267
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
2268
|
+
return fallback;
|
|
2269
|
+
}
|
|
2270
|
+
return Math.round(value);
|
|
2271
|
+
}
|
|
2272
|
+
function normalizeProviderUsage(raw) {
|
|
2273
|
+
if (!raw || typeof raw !== "object") {
|
|
2274
|
+
return void 0;
|
|
2275
|
+
}
|
|
2276
|
+
const value = raw;
|
|
2277
|
+
const input = value.inputTokens && typeof value.inputTokens === "object" ? value.inputTokens : {};
|
|
2278
|
+
const output2 = value.outputTokens && typeof value.outputTokens === "object" ? value.outputTokens : {};
|
|
2279
|
+
const cacheRead = toCount(input.cacheRead);
|
|
2280
|
+
const cacheWrite = toCount(input.cacheWrite);
|
|
2281
|
+
const uncached = toCount(input.noCache);
|
|
2282
|
+
const inputFromParts = uncached + cacheRead + cacheWrite;
|
|
2283
|
+
const inputTotal = Math.max(toCount(input.total, inputFromParts), inputFromParts);
|
|
2284
|
+
const reasoning = toCount(output2.reasoning);
|
|
2285
|
+
const outputTotal = Math.max(toCount(output2.total, toCount(output2.text) + reasoning), reasoning);
|
|
2286
|
+
if (inputTotal === 0 && outputTotal === 0 && cacheRead === 0 && cacheWrite === 0 && reasoning === 0) {
|
|
2287
|
+
return void 0;
|
|
2288
|
+
}
|
|
2289
|
+
return {
|
|
2290
|
+
inputTokens: inputTotal,
|
|
2291
|
+
outputTokens: outputTotal,
|
|
2292
|
+
cacheReadTokens: cacheRead,
|
|
2293
|
+
cacheWriteTokens: cacheWrite,
|
|
2294
|
+
reasoningTokens: reasoning,
|
|
2295
|
+
source: "provider"
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
function estimatedCompressionUsage(inputTokens, outputTokens) {
|
|
2299
|
+
return {
|
|
2300
|
+
inputTokens: toCount(inputTokens),
|
|
2301
|
+
outputTokens: toCount(outputTokens),
|
|
2302
|
+
cacheReadTokens: 0,
|
|
2303
|
+
cacheWriteTokens: 0,
|
|
2304
|
+
reasoningTokens: 0,
|
|
2305
|
+
source: "estimated"
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
2308
|
+
function addCompressionUsage(totals, usage) {
|
|
2309
|
+
return {
|
|
2310
|
+
calls: totals.calls + 1,
|
|
2311
|
+
providerCalls: totals.providerCalls + (usage.source === "provider" ? 1 : 0),
|
|
2312
|
+
estimatedCalls: totals.estimatedCalls + (usage.source === "estimated" ? 1 : 0),
|
|
2313
|
+
inputTokens: totals.inputTokens + usage.inputTokens,
|
|
2314
|
+
outputTokens: totals.outputTokens + usage.outputTokens,
|
|
2315
|
+
cacheReadTokens: totals.cacheReadTokens + usage.cacheReadTokens,
|
|
2316
|
+
cacheWriteTokens: totals.cacheWriteTokens + usage.cacheWriteTokens,
|
|
2317
|
+
reasoningTokens: totals.reasoningTokens + usage.reasoningTokens
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2320
|
+
function addCompressionUsageTotals(totals, other) {
|
|
2321
|
+
return {
|
|
2322
|
+
calls: totals.calls + other.calls,
|
|
2323
|
+
providerCalls: totals.providerCalls + other.providerCalls,
|
|
2324
|
+
estimatedCalls: totals.estimatedCalls + other.estimatedCalls,
|
|
2325
|
+
inputTokens: totals.inputTokens + other.inputTokens,
|
|
2326
|
+
outputTokens: totals.outputTokens + other.outputTokens,
|
|
2327
|
+
cacheReadTokens: totals.cacheReadTokens + other.cacheReadTokens,
|
|
2328
|
+
cacheWriteTokens: totals.cacheWriteTokens + other.cacheWriteTokens,
|
|
2329
|
+
reasoningTokens: totals.reasoningTokens + other.reasoningTokens
|
|
2330
|
+
};
|
|
2331
|
+
}
|
|
2332
|
+
function normalizeCompressionUsageTotals(value) {
|
|
2333
|
+
if (!value || typeof value !== "object") {
|
|
2334
|
+
return emptyCompressionUsage();
|
|
2335
|
+
}
|
|
2336
|
+
const raw = value;
|
|
2337
|
+
return {
|
|
2338
|
+
calls: toCount(raw.calls),
|
|
2339
|
+
providerCalls: toCount(raw.providerCalls),
|
|
2340
|
+
estimatedCalls: toCount(raw.estimatedCalls),
|
|
2341
|
+
inputTokens: toCount(raw.inputTokens),
|
|
2342
|
+
outputTokens: toCount(raw.outputTokens),
|
|
2343
|
+
cacheReadTokens: toCount(raw.cacheReadTokens),
|
|
2344
|
+
cacheWriteTokens: toCount(raw.cacheWriteTokens),
|
|
2345
|
+
reasoningTokens: toCount(raw.reasoningTokens)
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2253
2349
|
// lib/messages/shape.ts
|
|
2254
2350
|
function isMessageWithInfo(message) {
|
|
2255
2351
|
if (!message || typeof message !== "object") {
|
|
@@ -2824,7 +2920,8 @@ function emptyPersistedState(manualMode) {
|
|
|
2824
2920
|
},
|
|
2825
2921
|
stats: {
|
|
2826
2922
|
pruneTokenCounter: 0,
|
|
2827
|
-
totalPruneTokens: 0
|
|
2923
|
+
totalPruneTokens: 0,
|
|
2924
|
+
compressionUsage: emptyCompressionUsage()
|
|
2828
2925
|
},
|
|
2829
2926
|
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
2830
2927
|
};
|
|
@@ -2845,7 +2942,8 @@ async function loadAllSessionStats(logger) {
|
|
|
2845
2942
|
totalTokens: 0,
|
|
2846
2943
|
totalTools: 0,
|
|
2847
2944
|
totalMessages: 0,
|
|
2848
|
-
sessionCount: 0
|
|
2945
|
+
sessionCount: 0,
|
|
2946
|
+
compressionUsage: emptyCompressionUsage()
|
|
2849
2947
|
};
|
|
2850
2948
|
try {
|
|
2851
2949
|
if (!existsSync4(STORAGE_DIR)) {
|
|
@@ -2858,10 +2956,16 @@ async function loadAllSessionStats(logger) {
|
|
|
2858
2956
|
const filePath = join4(STORAGE_DIR, file);
|
|
2859
2957
|
const content = await fs.readFile(filePath, "utf-8");
|
|
2860
2958
|
const state = JSON.parse(content);
|
|
2861
|
-
|
|
2862
|
-
|
|
2959
|
+
const usage = normalizeCompressionUsageTotals(state?.stats?.compressionUsage);
|
|
2960
|
+
const hasUsage = usage.calls > 0;
|
|
2961
|
+
if (state?.prune && (state?.stats?.totalPruneTokens || hasUsage)) {
|
|
2962
|
+
result.totalTokens += state.stats?.totalPruneTokens || 0;
|
|
2863
2963
|
result.totalTools += state.prune.tools ? Object.keys(state.prune.tools).length : 0;
|
|
2864
2964
|
result.totalMessages += state.prune.messages?.byMessageId ? Object.keys(state.prune.messages.byMessageId).length : 0;
|
|
2965
|
+
result.compressionUsage = addCompressionUsageTotals(
|
|
2966
|
+
result.compressionUsage,
|
|
2967
|
+
usage
|
|
2968
|
+
);
|
|
2865
2969
|
result.sessionCount++;
|
|
2866
2970
|
}
|
|
2867
2971
|
} catch {
|
|
@@ -3302,7 +3406,8 @@ function createSessionState(idFormat = "xml") {
|
|
|
3302
3406
|
},
|
|
3303
3407
|
stats: {
|
|
3304
3408
|
pruneTokenCounter: 0,
|
|
3305
|
-
totalPruneTokens: 0
|
|
3409
|
+
totalPruneTokens: 0,
|
|
3410
|
+
compressionUsage: emptyCompressionUsage()
|
|
3306
3411
|
},
|
|
3307
3412
|
compressionTiming: {
|
|
3308
3413
|
startsByCallId: /* @__PURE__ */ new Map(),
|
|
@@ -3339,7 +3444,8 @@ function resetSessionState(state) {
|
|
|
3339
3444
|
};
|
|
3340
3445
|
state.stats = {
|
|
3341
3446
|
pruneTokenCounter: 0,
|
|
3342
|
-
totalPruneTokens: 0
|
|
3447
|
+
totalPruneTokens: 0,
|
|
3448
|
+
compressionUsage: emptyCompressionUsage()
|
|
3343
3449
|
};
|
|
3344
3450
|
state.toolParameters.clear();
|
|
3345
3451
|
state.subAgentResultCache.clear();
|
|
@@ -3385,7 +3491,8 @@ async function ensureSessionInitialized(client, state, sessionId, logger, messag
|
|
|
3385
3491
|
);
|
|
3386
3492
|
state.stats = {
|
|
3387
3493
|
pruneTokenCounter: persisted.stats?.pruneTokenCounter || 0,
|
|
3388
|
-
totalPruneTokens: persisted.stats?.totalPruneTokens || 0
|
|
3494
|
+
totalPruneTokens: persisted.stats?.totalPruneTokens || 0,
|
|
3495
|
+
compressionUsage: normalizeCompressionUsageTotals(persisted.stats?.compressionUsage)
|
|
3389
3496
|
};
|
|
3390
3497
|
const applied = applyPendingCompressionDurations(state);
|
|
3391
3498
|
if (applied > 0) {
|
|
@@ -4562,6 +4669,22 @@ ${output2}`);
|
|
|
4562
4669
|
|
|
4563
4670
|
// lib/compress/summary.ts
|
|
4564
4671
|
var RC_SUMMARY_MARKER = "[[DCP-RC-SUMMARY]]";
|
|
4672
|
+
var RC_SUMMARY_MARKER_PREFIX = "[[DCP-RC-SUMMARY";
|
|
4673
|
+
function sanitizeCallId(callId) {
|
|
4674
|
+
return callId.replace(/[^A-Za-z0-9_-]/g, "-");
|
|
4675
|
+
}
|
|
4676
|
+
function buildSummaryMarker(callId) {
|
|
4677
|
+
if (!callId) {
|
|
4678
|
+
return RC_SUMMARY_MARKER;
|
|
4679
|
+
}
|
|
4680
|
+
return `${RC_SUMMARY_MARKER_PREFIX}:${sanitizeCallId(callId)}]]`;
|
|
4681
|
+
}
|
|
4682
|
+
function hasSummaryMarker(text) {
|
|
4683
|
+
return text.includes(RC_SUMMARY_MARKER_PREFIX);
|
|
4684
|
+
}
|
|
4685
|
+
function extractSummaryCallId(text) {
|
|
4686
|
+
return text.match(/\[\[DCP-RC-SUMMARY:([A-Za-z0-9_-]+)\]\]/)?.[1];
|
|
4687
|
+
}
|
|
4565
4688
|
function stringify(value) {
|
|
4566
4689
|
if (typeof value === "string") {
|
|
4567
4690
|
return value;
|
|
@@ -4641,7 +4764,7 @@ var SUMMARY_RULES = `RULES
|
|
|
4641
4764
|
- Preserve every high-signal detail: user intent (quote short user messages exactly), decisions, constraints, file paths, function and symbol names, commands run, key findings, errors, verification results, and any subagent task IDs with their scope and completion state.
|
|
4642
4765
|
- Drop noise: failed attempts that led nowhere, repeated tool output, exploration that produced no durable result.
|
|
4643
4766
|
- Be dense and precise. Prefer terse technical wording over narrative.`;
|
|
4644
|
-
function buildSummaryPrompt(selectors, idFormat = "xml") {
|
|
4767
|
+
function buildSummaryPrompt(selectors, idFormat = "xml", callId) {
|
|
4645
4768
|
const selectorList = selectors.map((segment) => segment.selector);
|
|
4646
4769
|
const example = selectorList[0] ?? (idFormat === "compact" ? "" : "m0004");
|
|
4647
4770
|
const body = selectors.map((segment) => {
|
|
@@ -4652,7 +4775,7 @@ ${prior.body.trim()}`
|
|
|
4652
4775
|
).join("\n\n");
|
|
4653
4776
|
return [messageBody, priorBody].filter((part) => part.length > 0).join("\n\n");
|
|
4654
4777
|
}).join("\n\n");
|
|
4655
|
-
return `${
|
|
4778
|
+
return `${buildSummaryMarker(callId)}
|
|
4656
4779
|
|
|
4657
4780
|
You generate compressed-context entries for a coding session. Summarize ONLY the conversation segments listed below. Do not summarize anything else and do not ask questions.
|
|
4658
4781
|
|
|
@@ -4759,6 +4882,11 @@ function parseSummarySections(text, expected, idFormat = "xml") {
|
|
|
4759
4882
|
}
|
|
4760
4883
|
|
|
4761
4884
|
// lib/compress/rc.ts
|
|
4885
|
+
var summaryCallCounter = 0;
|
|
4886
|
+
function nextSummaryCallId() {
|
|
4887
|
+
summaryCallCounter += 1;
|
|
4888
|
+
return `c${summaryCallCounter.toString(36)}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
4889
|
+
}
|
|
4762
4890
|
function validateArgs(args) {
|
|
4763
4891
|
if (!args || typeof args !== "object") {
|
|
4764
4892
|
throw new Error("compress requires an object argument with an `ids` array");
|
|
@@ -4905,7 +5033,12 @@ function createRcCompressTool(ctx) {
|
|
|
4905
5033
|
messages: plan.messages,
|
|
4906
5034
|
priorSummaries: plan.priorSummaries
|
|
4907
5035
|
}));
|
|
4908
|
-
const text = await generateSummaries(ctx, sessionID, segments);
|
|
5036
|
+
const { text, usage } = await generateSummaries(ctx, sessionID, segments);
|
|
5037
|
+
ctx.state.stats.compressionUsage = addCompressionUsageTotals(
|
|
5038
|
+
ctx.state.stats.compressionUsage,
|
|
5039
|
+
usage
|
|
5040
|
+
);
|
|
5041
|
+
await saveSessionState(ctx.state, ctx.logger);
|
|
4909
5042
|
const accepted = await resolveAcceptedSections(
|
|
4910
5043
|
ctx,
|
|
4911
5044
|
plans.map((plan) => plan.selector.selector),
|
|
@@ -4944,8 +5077,19 @@ function createRcCompressTool(ctx) {
|
|
|
4944
5077
|
});
|
|
4945
5078
|
}
|
|
4946
5079
|
async function generateSummaries(ctx, sessionID, segments) {
|
|
4947
|
-
|
|
4948
|
-
|
|
5080
|
+
let usage = emptyCompressionUsage();
|
|
5081
|
+
const summarize = async (promptSegments) => {
|
|
5082
|
+
const callId = nextSummaryCallId();
|
|
5083
|
+
const prompt = buildSummaryPrompt(promptSegments, ctx.state.idFormat, callId);
|
|
5084
|
+
const result = await ctx.generate({ sessionID, prompt, callId });
|
|
5085
|
+
const text2 = result.text ?? "";
|
|
5086
|
+
usage = addCompressionUsage(
|
|
5087
|
+
usage,
|
|
5088
|
+
result.usage ?? estimatedCompressionUsage(countTokens2(prompt), countTokens2(text2))
|
|
5089
|
+
);
|
|
5090
|
+
return text2;
|
|
5091
|
+
};
|
|
5092
|
+
let text = await summarize(segments);
|
|
4949
5093
|
for (let retry = 0; retry < 2; retry++) {
|
|
4950
5094
|
const parsed = parseSummarySections(
|
|
4951
5095
|
text,
|
|
@@ -4962,14 +5106,13 @@ async function generateSummaries(ctx, sessionID, segments) {
|
|
|
4962
5106
|
if (retrySegments.length === 0) {
|
|
4963
5107
|
break;
|
|
4964
5108
|
}
|
|
4965
|
-
const retryPrompt = buildSummaryPrompt(retrySegments, ctx.state.idFormat);
|
|
4966
5109
|
try {
|
|
4967
|
-
text += "\n\n" + await
|
|
5110
|
+
text += "\n\n" + await summarize(retrySegments);
|
|
4968
5111
|
} catch {
|
|
4969
5112
|
break;
|
|
4970
5113
|
}
|
|
4971
5114
|
}
|
|
4972
|
-
return text;
|
|
5115
|
+
return { text, usage };
|
|
4973
5116
|
}
|
|
4974
5117
|
async function resolveAcceptedSections(ctx, selectors, text) {
|
|
4975
5118
|
const parsed = parseSummarySections(text, selectors, ctx.state.idFormat);
|
|
@@ -5081,6 +5224,318 @@ async function resetManualMode(ctx, sessionID) {
|
|
|
5081
5224
|
await saveSessionState(ctx.state, ctx.logger);
|
|
5082
5225
|
}
|
|
5083
5226
|
|
|
5227
|
+
// lib/v2/usage.ts
|
|
5228
|
+
function createCompressionUsageTracker() {
|
|
5229
|
+
const pending = /* @__PURE__ */ new Map();
|
|
5230
|
+
const entry = (callId) => {
|
|
5231
|
+
let current = pending.get(callId);
|
|
5232
|
+
if (!current) {
|
|
5233
|
+
current = {};
|
|
5234
|
+
pending.set(callId, current);
|
|
5235
|
+
}
|
|
5236
|
+
return current;
|
|
5237
|
+
};
|
|
5238
|
+
return {
|
|
5239
|
+
recordProvider(callId, usage) {
|
|
5240
|
+
entry(callId).provider = usage;
|
|
5241
|
+
},
|
|
5242
|
+
recordEstimatedInput(callId, inputTokens) {
|
|
5243
|
+
if (Number.isFinite(inputTokens) && inputTokens > 0) {
|
|
5244
|
+
entry(callId).estimatedInput = Math.round(inputTokens);
|
|
5245
|
+
}
|
|
5246
|
+
},
|
|
5247
|
+
resolve(callId, outputText) {
|
|
5248
|
+
const current = pending.get(callId);
|
|
5249
|
+
if (!current) {
|
|
5250
|
+
return void 0;
|
|
5251
|
+
}
|
|
5252
|
+
if (current.provider) {
|
|
5253
|
+
return current.provider;
|
|
5254
|
+
}
|
|
5255
|
+
if (current.estimatedInput === void 0) {
|
|
5256
|
+
return void 0;
|
|
5257
|
+
}
|
|
5258
|
+
return estimatedCompressionUsage(current.estimatedInput, countTokens2(outputText));
|
|
5259
|
+
},
|
|
5260
|
+
discard(callId) {
|
|
5261
|
+
pending.delete(callId);
|
|
5262
|
+
}
|
|
5263
|
+
};
|
|
5264
|
+
}
|
|
5265
|
+
function findSummaryCallId(options) {
|
|
5266
|
+
const texts = [];
|
|
5267
|
+
collectTexts(options?.prompt, texts);
|
|
5268
|
+
for (const text of texts) {
|
|
5269
|
+
const callId = extractSummaryCallId(text);
|
|
5270
|
+
if (callId) {
|
|
5271
|
+
return callId;
|
|
5272
|
+
}
|
|
5273
|
+
}
|
|
5274
|
+
return void 0;
|
|
5275
|
+
}
|
|
5276
|
+
function collectTexts(value, out, depth = 0) {
|
|
5277
|
+
if (depth > 8 || out.length > 200) {
|
|
5278
|
+
return;
|
|
5279
|
+
}
|
|
5280
|
+
if (typeof value === "string") {
|
|
5281
|
+
out.push(value);
|
|
5282
|
+
return;
|
|
5283
|
+
}
|
|
5284
|
+
if (Array.isArray(value)) {
|
|
5285
|
+
for (const item of value) {
|
|
5286
|
+
collectTexts(item, out, depth + 1);
|
|
5287
|
+
}
|
|
5288
|
+
return;
|
|
5289
|
+
}
|
|
5290
|
+
if (value && typeof value === "object") {
|
|
5291
|
+
const record = value;
|
|
5292
|
+
if (typeof record.text === "string") {
|
|
5293
|
+
out.push(record.text);
|
|
5294
|
+
}
|
|
5295
|
+
if (typeof record.content === "string") {
|
|
5296
|
+
out.push(record.content);
|
|
5297
|
+
} else if (Array.isArray(record.content)) {
|
|
5298
|
+
collectTexts(record.content, out, depth + 1);
|
|
5299
|
+
}
|
|
5300
|
+
}
|
|
5301
|
+
}
|
|
5302
|
+
function wrapLanguageModel(model, tracker, logger) {
|
|
5303
|
+
return new Proxy(model, {
|
|
5304
|
+
get(target, property, receiver) {
|
|
5305
|
+
if (property === "doStream") {
|
|
5306
|
+
return (options) => trackedStream(target, options, tracker, logger);
|
|
5307
|
+
}
|
|
5308
|
+
if (property === "doGenerate") {
|
|
5309
|
+
return (options) => trackedGenerate(target, options, tracker, logger);
|
|
5310
|
+
}
|
|
5311
|
+
return Reflect.get(target, property, receiver);
|
|
5312
|
+
}
|
|
5313
|
+
});
|
|
5314
|
+
}
|
|
5315
|
+
async function trackedStream(target, options, tracker, logger) {
|
|
5316
|
+
const result = await target.doStream(options);
|
|
5317
|
+
const callId = findSummaryCallId(options);
|
|
5318
|
+
logger.debug("Compression usage stream call", { callId: callId ?? null });
|
|
5319
|
+
const TransformStreamImpl = globalThis.TransformStream;
|
|
5320
|
+
if (!callId || typeof result?.stream?.pipeThrough !== "function") {
|
|
5321
|
+
return result;
|
|
5322
|
+
}
|
|
5323
|
+
if (typeof TransformStreamImpl !== "function") {
|
|
5324
|
+
return result;
|
|
5325
|
+
}
|
|
5326
|
+
let recorded = false;
|
|
5327
|
+
const transform = new TransformStreamImpl({
|
|
5328
|
+
transform(part, controller) {
|
|
5329
|
+
try {
|
|
5330
|
+
if (!recorded && part?.type === "finish" && part.usage) {
|
|
5331
|
+
const usage = normalizeProviderUsage(part.usage);
|
|
5332
|
+
if (usage) {
|
|
5333
|
+
recorded = true;
|
|
5334
|
+
tracker.recordProvider(callId, usage);
|
|
5335
|
+
}
|
|
5336
|
+
}
|
|
5337
|
+
} catch {
|
|
5338
|
+
}
|
|
5339
|
+
controller.enqueue(part);
|
|
5340
|
+
}
|
|
5341
|
+
});
|
|
5342
|
+
return { ...result, stream: result.stream.pipeThrough(transform) };
|
|
5343
|
+
}
|
|
5344
|
+
async function trackedGenerate(target, options, tracker, logger) {
|
|
5345
|
+
const result = await target.doGenerate(options);
|
|
5346
|
+
const callId = findSummaryCallId(options);
|
|
5347
|
+
logger.debug("Compression usage generate call", { callId: callId ?? null });
|
|
5348
|
+
if (!callId) {
|
|
5349
|
+
return result;
|
|
5350
|
+
}
|
|
5351
|
+
const usage = normalizeProviderUsage(result?.usage);
|
|
5352
|
+
if (usage) {
|
|
5353
|
+
tracker.recordProvider(callId, usage);
|
|
5354
|
+
}
|
|
5355
|
+
return result;
|
|
5356
|
+
}
|
|
5357
|
+
async function installCompressionUsageHook(ctx, tracker, logger) {
|
|
5358
|
+
const aisdk = ctx?.aisdk;
|
|
5359
|
+
if (!aisdk || typeof aisdk.hook !== "function") {
|
|
5360
|
+
logger.debug("aisdk hook unavailable; compression usage will be estimated");
|
|
5361
|
+
return;
|
|
5362
|
+
}
|
|
5363
|
+
try {
|
|
5364
|
+
await aisdk.hook("language", (event) => {
|
|
5365
|
+
try {
|
|
5366
|
+
const isModel = (candidate) => !!candidate && typeof candidate.doStream === "function";
|
|
5367
|
+
logger.debug("Compression usage hook event", {
|
|
5368
|
+
modelID: event?.model?.modelID ?? event?.model?.id ?? null,
|
|
5369
|
+
hasLanguage: isModel(event?.language),
|
|
5370
|
+
hasFactory: typeof event?.sdk?.languageModel === "function"
|
|
5371
|
+
});
|
|
5372
|
+
let model = isModel(event?.language) ? event.language : void 0;
|
|
5373
|
+
if (!model && typeof event?.sdk?.languageModel === "function") {
|
|
5374
|
+
model = event.sdk.languageModel(
|
|
5375
|
+
event?.model?.modelID ?? event?.model?.id
|
|
5376
|
+
);
|
|
5377
|
+
}
|
|
5378
|
+
if (!isModel(model)) {
|
|
5379
|
+
return;
|
|
5380
|
+
}
|
|
5381
|
+
event.language = wrapLanguageModel(model, tracker, logger);
|
|
5382
|
+
} catch (error) {
|
|
5383
|
+
logger.debug("Compression usage hook could not wrap language model", {
|
|
5384
|
+
error: error?.message
|
|
5385
|
+
});
|
|
5386
|
+
}
|
|
5387
|
+
});
|
|
5388
|
+
logger.debug("Compression usage hook installed");
|
|
5389
|
+
} catch (error) {
|
|
5390
|
+
logger.warn("Compression usage hook registration failed", {
|
|
5391
|
+
error: error?.message
|
|
5392
|
+
});
|
|
5393
|
+
}
|
|
5394
|
+
}
|
|
5395
|
+
function emptyUsageAccumulator() {
|
|
5396
|
+
return {
|
|
5397
|
+
input: 0,
|
|
5398
|
+
output: 0,
|
|
5399
|
+
cacheRead: 0,
|
|
5400
|
+
cacheWrite: 0,
|
|
5401
|
+
reasoning: 0,
|
|
5402
|
+
exclusiveInput: false
|
|
5403
|
+
};
|
|
5404
|
+
}
|
|
5405
|
+
function usageField(key) {
|
|
5406
|
+
switch (key) {
|
|
5407
|
+
case "input_tokens":
|
|
5408
|
+
case "prompt_tokens":
|
|
5409
|
+
case "promptTokenCount":
|
|
5410
|
+
return "input";
|
|
5411
|
+
case "output_tokens":
|
|
5412
|
+
case "completion_tokens":
|
|
5413
|
+
case "candidatesTokenCount":
|
|
5414
|
+
return "output";
|
|
5415
|
+
case "cache_read_input_tokens":
|
|
5416
|
+
case "cache_creation_input_tokens":
|
|
5417
|
+
case "cached_tokens":
|
|
5418
|
+
case "cachedContentTokenCount":
|
|
5419
|
+
return key === "cache_creation_input_tokens" ? "cacheWrite" : "cacheRead";
|
|
5420
|
+
case "reasoning_tokens":
|
|
5421
|
+
case "thoughtsTokenCount":
|
|
5422
|
+
return "reasoning";
|
|
5423
|
+
default:
|
|
5424
|
+
return void 0;
|
|
5425
|
+
}
|
|
5426
|
+
}
|
|
5427
|
+
function scanUsage(value, acc, depth) {
|
|
5428
|
+
if (depth > 8 || !value || typeof value !== "object") {
|
|
5429
|
+
return;
|
|
5430
|
+
}
|
|
5431
|
+
if (Array.isArray(value)) {
|
|
5432
|
+
for (const item of value) {
|
|
5433
|
+
scanUsage(item, acc, depth + 1);
|
|
5434
|
+
}
|
|
5435
|
+
return;
|
|
5436
|
+
}
|
|
5437
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
5438
|
+
const field = usageField(key);
|
|
5439
|
+
if (field && typeof raw === "number" && Number.isFinite(raw)) {
|
|
5440
|
+
acc[field] = Math.max(acc[field], raw);
|
|
5441
|
+
if (key === "cache_read_input_tokens" || key === "cache_creation_input_tokens") {
|
|
5442
|
+
acc.exclusiveInput = true;
|
|
5443
|
+
}
|
|
5444
|
+
continue;
|
|
5445
|
+
}
|
|
5446
|
+
if (raw && typeof raw === "object") {
|
|
5447
|
+
scanUsage(raw, acc, depth + 1);
|
|
5448
|
+
}
|
|
5449
|
+
}
|
|
5450
|
+
}
|
|
5451
|
+
function usageFromAccumulator(acc) {
|
|
5452
|
+
const input = acc.exclusiveInput ? acc.input + acc.cacheRead + acc.cacheWrite : acc.input;
|
|
5453
|
+
const usage = {
|
|
5454
|
+
inputTokens: Math.max(0, Math.round(input)),
|
|
5455
|
+
outputTokens: Math.max(0, Math.round(acc.output)),
|
|
5456
|
+
cacheReadTokens: Math.max(0, Math.round(acc.cacheRead)),
|
|
5457
|
+
cacheWriteTokens: Math.max(0, Math.round(acc.cacheWrite)),
|
|
5458
|
+
reasoningTokens: Math.max(0, Math.round(acc.reasoning)),
|
|
5459
|
+
source: "provider"
|
|
5460
|
+
};
|
|
5461
|
+
const total = usage.inputTokens + usage.outputTokens + usage.cacheReadTokens + usage.cacheWriteTokens + usage.reasoningTokens;
|
|
5462
|
+
return total > 0 ? usage : void 0;
|
|
5463
|
+
}
|
|
5464
|
+
function parseProviderUsageFromBody(body) {
|
|
5465
|
+
const acc = emptyUsageAccumulator();
|
|
5466
|
+
const trimmed = body.trim();
|
|
5467
|
+
let parsedJson = false;
|
|
5468
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
5469
|
+
try {
|
|
5470
|
+
scanUsage(JSON.parse(trimmed), acc, 0);
|
|
5471
|
+
parsedJson = true;
|
|
5472
|
+
} catch {
|
|
5473
|
+
}
|
|
5474
|
+
}
|
|
5475
|
+
if (!parsedJson) {
|
|
5476
|
+
for (const line of body.split(/\r?\n/)) {
|
|
5477
|
+
const match = /^\s*data:\s*(.*)$/.exec(line);
|
|
5478
|
+
if (!match) {
|
|
5479
|
+
continue;
|
|
5480
|
+
}
|
|
5481
|
+
const payload = match[1].trim();
|
|
5482
|
+
if (!payload || payload === "[DONE]") {
|
|
5483
|
+
continue;
|
|
5484
|
+
}
|
|
5485
|
+
try {
|
|
5486
|
+
scanUsage(JSON.parse(payload), acc, 0);
|
|
5487
|
+
} catch {
|
|
5488
|
+
}
|
|
5489
|
+
}
|
|
5490
|
+
}
|
|
5491
|
+
return usageFromAccumulator(acc);
|
|
5492
|
+
}
|
|
5493
|
+
async function installHttpUsageHook(ctx, tracker, logger) {
|
|
5494
|
+
const session2 = ctx?.session;
|
|
5495
|
+
if (!session2 || typeof session2.hook !== "function") {
|
|
5496
|
+
return;
|
|
5497
|
+
}
|
|
5498
|
+
try {
|
|
5499
|
+
await session2.hook("http.response", async (event) => {
|
|
5500
|
+
if (event?.kind !== "generate") {
|
|
5501
|
+
return;
|
|
5502
|
+
}
|
|
5503
|
+
try {
|
|
5504
|
+
const request = event.request;
|
|
5505
|
+
const response = event.response;
|
|
5506
|
+
if (!request || typeof request.clone !== "function") {
|
|
5507
|
+
return;
|
|
5508
|
+
}
|
|
5509
|
+
const callId = extractSummaryCallId(await request.clone().text());
|
|
5510
|
+
if (!callId) {
|
|
5511
|
+
return;
|
|
5512
|
+
}
|
|
5513
|
+
if (!response || typeof response.clone !== "function") {
|
|
5514
|
+
return;
|
|
5515
|
+
}
|
|
5516
|
+
const usage = parseProviderUsageFromBody(await response.clone().text());
|
|
5517
|
+
if (usage) {
|
|
5518
|
+
tracker.recordProvider(callId, usage);
|
|
5519
|
+
logger.debug("Compression usage captured from http response", {
|
|
5520
|
+
callId,
|
|
5521
|
+
inputTokens: usage.inputTokens,
|
|
5522
|
+
outputTokens: usage.outputTokens
|
|
5523
|
+
});
|
|
5524
|
+
}
|
|
5525
|
+
} catch (error) {
|
|
5526
|
+
logger.debug("Compression usage http capture failed", {
|
|
5527
|
+
error: error?.message
|
|
5528
|
+
});
|
|
5529
|
+
}
|
|
5530
|
+
});
|
|
5531
|
+
logger.debug("Compression usage http hook installed");
|
|
5532
|
+
} catch (error) {
|
|
5533
|
+
logger.warn("Compression usage http hook registration failed", {
|
|
5534
|
+
error: error?.message
|
|
5535
|
+
});
|
|
5536
|
+
}
|
|
5537
|
+
}
|
|
5538
|
+
|
|
5084
5539
|
// lib/prompts/index.ts
|
|
5085
5540
|
function renderSystemPrompt(prompts, protectedToolsExtension, manual, subagent) {
|
|
5086
5541
|
const extensions = [];
|
|
@@ -6592,7 +7047,7 @@ async function handleRecompressCommand(ctx) {
|
|
|
6592
7047
|
}
|
|
6593
7048
|
|
|
6594
7049
|
// lib/commands/stats.ts
|
|
6595
|
-
function formatStatsMessage(sessionTokens, sessionSummaryTokens, sessionTools, sessionMessages, sessionDurationMs, allTime) {
|
|
7050
|
+
function formatStatsMessage(sessionTokens, sessionSummaryTokens, sessionTools, sessionMessages, sessionDurationMs, sessionUsage, allTime) {
|
|
6596
7051
|
const lines = [];
|
|
6597
7052
|
lines.push("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E");
|
|
6598
7053
|
lines.push("\u2502 DCP Statistics \u2502");
|
|
@@ -6607,6 +7062,16 @@ function formatStatsMessage(sessionTokens, sessionSummaryTokens, sessionTools, s
|
|
|
6607
7062
|
lines.push(` Time: ${formatCompressionTime(sessionDurationMs)}`);
|
|
6608
7063
|
lines.push(` Messages: ${sessionMessages}`);
|
|
6609
7064
|
lines.push(` Tools: ${sessionTools}`);
|
|
7065
|
+
lines.push(
|
|
7066
|
+
` Summary requests: ${sessionUsage.calls} (${sessionUsage.providerCalls} provider, ${sessionUsage.estimatedCalls} estimated)`
|
|
7067
|
+
);
|
|
7068
|
+
lines.push(
|
|
7069
|
+
` Request in|out: ~${formatTokenCount(sessionUsage.inputTokens)} | ~${formatTokenCount(sessionUsage.outputTokens)}`
|
|
7070
|
+
);
|
|
7071
|
+
lines.push(
|
|
7072
|
+
` Request cache: ~${formatTokenCount(sessionUsage.cacheReadTokens)} read | ~${formatTokenCount(sessionUsage.cacheWriteTokens)} write`
|
|
7073
|
+
);
|
|
7074
|
+
lines.push(` Request reason: ~${formatTokenCount(sessionUsage.reasoningTokens)}`);
|
|
6610
7075
|
lines.push("");
|
|
6611
7076
|
lines.push("All-time:");
|
|
6612
7077
|
lines.push("\u2500".repeat(60));
|
|
@@ -6614,6 +7079,9 @@ function formatStatsMessage(sessionTokens, sessionSummaryTokens, sessionTools, s
|
|
|
6614
7079
|
lines.push(` Tools pruned: ${allTime.totalTools}`);
|
|
6615
7080
|
lines.push(` Messages pruned: ${allTime.totalMessages}`);
|
|
6616
7081
|
lines.push(` Sessions: ${allTime.sessionCount}`);
|
|
7082
|
+
lines.push(
|
|
7083
|
+
` Summary requests: ${allTime.compressionUsage.calls} (~${formatTokenCount(allTime.compressionUsage.inputTokens)} in | ~${formatTokenCount(allTime.compressionUsage.outputTokens)} out)`
|
|
7084
|
+
);
|
|
6617
7085
|
return lines.join("\n");
|
|
6618
7086
|
}
|
|
6619
7087
|
function formatCompressionRatio(inputTokens, outputTokens) {
|
|
@@ -6653,6 +7121,7 @@ async function handleStatsCommand(ctx) {
|
|
|
6653
7121
|
report2.sessionTools,
|
|
6654
7122
|
report2.sessionMessages,
|
|
6655
7123
|
report2.sessionDurationMs,
|
|
7124
|
+
report2.sessionCompressionUsage,
|
|
6656
7125
|
report2.allTime
|
|
6657
7126
|
);
|
|
6658
7127
|
const params = getCurrentParams(state, messages, logger);
|
|
@@ -6663,9 +7132,11 @@ async function handleStatsCommand(ctx) {
|
|
|
6663
7132
|
sessionTools: report2.sessionTools,
|
|
6664
7133
|
sessionMessages: report2.sessionMessages,
|
|
6665
7134
|
sessionDurationMs: report2.sessionDurationMs,
|
|
7135
|
+
sessionCompressionUsage: report2.sessionCompressionUsage,
|
|
6666
7136
|
allTimeTokens: report2.allTime.totalTokens,
|
|
6667
7137
|
allTimeTools: report2.allTime.totalTools,
|
|
6668
|
-
allTimeMessages: report2.allTime.totalMessages
|
|
7138
|
+
allTimeMessages: report2.allTime.totalMessages,
|
|
7139
|
+
allTimeCompressionUsage: report2.allTime.compressionUsage
|
|
6669
7140
|
});
|
|
6670
7141
|
}
|
|
6671
7142
|
async function buildStatsReport(state, logger) {
|
|
@@ -6687,6 +7158,7 @@ async function buildStatsReport(state, logger) {
|
|
|
6687
7158
|
}
|
|
6688
7159
|
}
|
|
6689
7160
|
const sessionTools = prunedToolIds.size;
|
|
7161
|
+
const sessionCompressionUsage = normalizeCompressionUsageTotals(state.stats.compressionUsage);
|
|
6690
7162
|
let sessionMessages = 0;
|
|
6691
7163
|
for (const entry of state.prune.messages.byMessageId.values()) {
|
|
6692
7164
|
if (entry.activeBlockIds.length > 0) {
|
|
@@ -6700,6 +7172,7 @@ async function buildStatsReport(state, logger) {
|
|
|
6700
7172
|
sessionTools,
|
|
6701
7173
|
sessionMessages,
|
|
6702
7174
|
sessionDurationMs,
|
|
7175
|
+
sessionCompressionUsage,
|
|
6703
7176
|
allTime
|
|
6704
7177
|
};
|
|
6705
7178
|
}
|
|
@@ -7235,17 +7708,29 @@ function project(native, entries, session2) {
|
|
|
7235
7708
|
import { tool as tool2 } from "@opencode-ai/plugin";
|
|
7236
7709
|
var z = tool2.schema;
|
|
7237
7710
|
var session = z.object({ sessionID: z.string() });
|
|
7711
|
+
var compressionUsage = z.object({
|
|
7712
|
+
calls: z.number(),
|
|
7713
|
+
providerCalls: z.number(),
|
|
7714
|
+
estimatedCalls: z.number(),
|
|
7715
|
+
inputTokens: z.number(),
|
|
7716
|
+
outputTokens: z.number(),
|
|
7717
|
+
cacheReadTokens: z.number(),
|
|
7718
|
+
cacheWriteTokens: z.number(),
|
|
7719
|
+
reasoningTokens: z.number()
|
|
7720
|
+
});
|
|
7238
7721
|
var stats = z.object({
|
|
7239
7722
|
sessionTokens: z.number(),
|
|
7240
7723
|
sessionSummaryTokens: z.number(),
|
|
7241
7724
|
sessionDurationMs: z.number(),
|
|
7242
7725
|
sessionTools: z.number(),
|
|
7243
7726
|
sessionMessages: z.number(),
|
|
7727
|
+
sessionCompressionUsage: compressionUsage,
|
|
7244
7728
|
allTime: z.object({
|
|
7245
7729
|
totalTokens: z.number(),
|
|
7246
7730
|
totalTools: z.number(),
|
|
7247
7731
|
totalMessages: z.number(),
|
|
7248
|
-
sessionCount: z.number()
|
|
7732
|
+
sessionCount: z.number(),
|
|
7733
|
+
compressionUsage
|
|
7249
7734
|
})
|
|
7250
7735
|
});
|
|
7251
7736
|
var context = z.object({
|
|
@@ -7300,6 +7785,9 @@ async function setup(ctx) {
|
|
|
7300
7785
|
return;
|
|
7301
7786
|
}
|
|
7302
7787
|
const logger = new Logger(config.debug);
|
|
7788
|
+
const usageTracker = createCompressionUsageTracker();
|
|
7789
|
+
await installCompressionUsageHook(ctx, usageTracker, logger);
|
|
7790
|
+
await installHttpUsageHook(ctx, usageTracker, logger);
|
|
7303
7791
|
const prompts = new PromptStore(
|
|
7304
7792
|
logger,
|
|
7305
7793
|
ctx.location.directory,
|
|
@@ -7465,7 +7953,7 @@ async function setup(ctx) {
|
|
|
7465
7953
|
await ctx.session.hook("generate", (event) => {
|
|
7466
7954
|
const own = event.messages.filter(
|
|
7467
7955
|
(message) => Array.isArray(message.content) && message.content.some(
|
|
7468
|
-
(part) => part?.type === "text" && typeof part.text === "string" && part.text
|
|
7956
|
+
(part) => part?.type === "text" && typeof part.text === "string" && hasSummaryMarker(part.text)
|
|
7469
7957
|
)
|
|
7470
7958
|
);
|
|
7471
7959
|
if (own.length === 0) {
|
|
@@ -7475,6 +7963,25 @@ async function setup(ctx) {
|
|
|
7475
7963
|
for (const name of Object.keys(event.tools ?? {})) {
|
|
7476
7964
|
delete event.tools[name];
|
|
7477
7965
|
}
|
|
7966
|
+
const callId = own.flatMap(
|
|
7967
|
+
(message) => Array.isArray(message.content) ? message.content : []
|
|
7968
|
+
).filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => extractSummaryCallId(part.text)).find((id) => !!id);
|
|
7969
|
+
if (callId) {
|
|
7970
|
+
const texts = [];
|
|
7971
|
+
for (const part of event.system ?? []) {
|
|
7972
|
+
if (typeof part?.text === "string") {
|
|
7973
|
+
texts.push(part.text);
|
|
7974
|
+
}
|
|
7975
|
+
}
|
|
7976
|
+
for (const message of own) {
|
|
7977
|
+
for (const part of message.content ?? []) {
|
|
7978
|
+
if (part?.type === "text" && typeof part.text === "string") {
|
|
7979
|
+
texts.push(part.text);
|
|
7980
|
+
}
|
|
7981
|
+
}
|
|
7982
|
+
}
|
|
7983
|
+
usageTracker.recordEstimatedInput(callId, countTokens2(texts.join("\n")));
|
|
7984
|
+
}
|
|
7478
7985
|
});
|
|
7479
7986
|
if (config.compress.permission !== "deny") {
|
|
7480
7987
|
const define = (state) => {
|
|
@@ -7485,8 +7992,19 @@ async function setup(ctx) {
|
|
|
7485
7992
|
config,
|
|
7486
7993
|
prompts,
|
|
7487
7994
|
generate: async (input) => {
|
|
7488
|
-
|
|
7489
|
-
|
|
7995
|
+
try {
|
|
7996
|
+
const result = await ctx.session.generate({
|
|
7997
|
+
sessionID: input.sessionID,
|
|
7998
|
+
prompt: input.prompt
|
|
7999
|
+
});
|
|
8000
|
+
const text = result?.text ?? "";
|
|
8001
|
+
const usage = input.callId ? usageTracker.resolve(input.callId, text) : void 0;
|
|
8002
|
+
return { text, usage };
|
|
8003
|
+
} finally {
|
|
8004
|
+
if (input.callId) {
|
|
8005
|
+
usageTracker.discard(input.callId);
|
|
8006
|
+
}
|
|
8007
|
+
}
|
|
7490
8008
|
}
|
|
7491
8009
|
};
|
|
7492
8010
|
return createRcCompressTool(context2);
|