opencode-acp 1.12.10-dev.1 → 1.13.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/NOTICE +29 -0
- package/README.md +120 -40
- package/README.zh-CN.md +98 -24
- package/dist/index.js +933 -101
- package/dist/index.js.map +1 -1
- package/dist/lib/compress/pipeline.d.ts.map +1 -1
- package/dist/lib/compress/quality-gate/algorithms/index.d.ts +3 -0
- package/dist/lib/compress/quality-gate/algorithms/index.d.ts.map +1 -0
- package/dist/lib/compress/quality-gate/evaluate.d.ts +8 -0
- package/dist/lib/compress/quality-gate/evaluate.d.ts.map +1 -0
- package/dist/lib/compress/quality-gate/index.d.ts +5 -0
- package/dist/lib/compress/quality-gate/index.d.ts.map +1 -0
- package/dist/lib/compress/quality-gate/registry.d.ts +6 -0
- package/dist/lib/compress/quality-gate/registry.d.ts.map +1 -0
- package/dist/lib/compress/quality-gate/types.d.ts +51 -0
- package/dist/lib/compress/quality-gate/types.d.ts.map +1 -0
- package/dist/lib/compress/range-utils.d.ts.map +1 -1
- package/dist/lib/compress/range.d.ts.map +1 -1
- package/dist/lib/compress/types.d.ts +5 -2
- package/dist/lib/compress/types.d.ts.map +1 -1
- package/dist/lib/config-validation.d.ts.map +1 -1
- package/dist/lib/config.d.ts +9 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/hooks.d.ts.map +1 -1
- package/dist/lib/messages/inject/policy/index.d.ts +5 -0
- package/dist/lib/messages/inject/policy/index.d.ts.map +1 -0
- package/dist/lib/messages/inject/policy/registry.d.ts +8 -0
- package/dist/lib/messages/inject/policy/registry.d.ts.map +1 -0
- package/dist/lib/messages/inject/policy/types.d.ts +2 -0
- package/dist/lib/messages/inject/policy/types.d.ts.map +1 -0
- package/dist/lib/messages/inject/utils.d.ts +0 -9
- package/dist/lib/messages/inject/utils.d.ts.map +1 -1
- package/dist/lib/messages/utils.d.ts.map +1 -1
- package/dist/lib/prompts/compress-range.d.ts +1 -1
- package/dist/lib/prompts/compress-range.d.ts.map +1 -1
- package/dist/lib/prompts/extensions/nudge.d.ts.map +1 -1
- package/dist/lib/prompts/extensions/tool.d.ts +1 -1
- package/dist/lib/prompts/extensions/tool.d.ts.map +1 -1
- package/dist/lib/prompts/system.d.ts +1 -1
- package/dist/lib/prompts/system.d.ts.map +1 -1
- package/dist/lib/ui/notification.d.ts.map +1 -1
- package/package.json +5 -2
- package/dist/lib/prompts/compression-rules.d.ts +0 -20
- package/dist/lib/prompts/compression-rules.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -926,7 +926,11 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
|
926
926
|
"strategies.purgeErrors",
|
|
927
927
|
"strategies.purgeErrors.enabled",
|
|
928
928
|
"strategies.purgeErrors.turns",
|
|
929
|
-
"strategies.purgeErrors.protectedTools"
|
|
929
|
+
"strategies.purgeErrors.protectedTools",
|
|
930
|
+
"qualityGate",
|
|
931
|
+
"qualityGate.enabled",
|
|
932
|
+
"qualityGate.algorithm",
|
|
933
|
+
"qualityGate.algorithms"
|
|
930
934
|
]);
|
|
931
935
|
function getConfigKeyPaths(obj, prefix = "") {
|
|
932
936
|
const keys = [];
|
|
@@ -1554,7 +1558,10 @@ var defaultConfig = {
|
|
|
1554
1558
|
autoUpdate: true,
|
|
1555
1559
|
debug: false,
|
|
1556
1560
|
pruneNotification: "detailed",
|
|
1557
|
-
|
|
1561
|
+
// [FIX #20] Default to toast — chat-mode notifications inject an empty
|
|
1562
|
+
// user message that freezes the session on providers that reject empty
|
|
1563
|
+
// messages (zhipuai-lb code 1214). See lib/ui/notification.ts.
|
|
1564
|
+
pruneNotificationType: "toast",
|
|
1558
1565
|
commands: {
|
|
1559
1566
|
enabled: true,
|
|
1560
1567
|
protectedTools: [...DEFAULT_PROTECTED_TOOLS]
|
|
@@ -1609,7 +1616,8 @@ var defaultConfig = {
|
|
|
1609
1616
|
gc: {
|
|
1610
1617
|
algorithm: "truncate",
|
|
1611
1618
|
promotionThreshold: 5,
|
|
1612
|
-
maxBlockAge:
|
|
1619
|
+
maxBlockAge: Number.MAX_SAFE_INTEGER,
|
|
1620
|
+
// no-op: age-based deactivation removed (memory-loss fix)
|
|
1613
1621
|
maxOldGenSummaryLength: 3e3,
|
|
1614
1622
|
majorGcThresholdPercent: "100%",
|
|
1615
1623
|
batchCleanup: {
|
|
@@ -1617,6 +1625,18 @@ var defaultConfig = {
|
|
|
1617
1625
|
highThreshold: "75%",
|
|
1618
1626
|
forceThreshold: "90%"
|
|
1619
1627
|
}
|
|
1628
|
+
},
|
|
1629
|
+
qualityGate: {
|
|
1630
|
+
enabled: false,
|
|
1631
|
+
algorithm: "rouge-recall-v1",
|
|
1632
|
+
algorithms: {
|
|
1633
|
+
"rouge-recall-v1": {
|
|
1634
|
+
layer1MinChars: 200,
|
|
1635
|
+
layer1MinRetentionPct: 1,
|
|
1636
|
+
layer2MaxRougeF1: 0.05,
|
|
1637
|
+
layer2MaxTop20Recall: 0.2
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1620
1640
|
}
|
|
1621
1641
|
};
|
|
1622
1642
|
var GLOBAL_CONFIG_DIR = process.env.XDG_CONFIG_HOME ? join(process.env.XDG_CONFIG_HOME, "opencode") : join(homedir(), ".config", "opencode");
|
|
@@ -1814,6 +1834,11 @@ function deepCloneConfig(config) {
|
|
|
1814
1834
|
gc: {
|
|
1815
1835
|
...config.gc,
|
|
1816
1836
|
batchCleanup: { ...config.gc.batchCleanup }
|
|
1837
|
+
},
|
|
1838
|
+
qualityGate: {
|
|
1839
|
+
enabled: config.qualityGate.enabled,
|
|
1840
|
+
algorithm: config.qualityGate.algorithm,
|
|
1841
|
+
algorithms: { ...config.qualityGate.algorithms }
|
|
1817
1842
|
}
|
|
1818
1843
|
};
|
|
1819
1844
|
}
|
|
@@ -1827,6 +1852,14 @@ function mergeGC(base, override) {
|
|
|
1827
1852
|
batchCleanup: { ...base.batchCleanup, ...override.batchCleanup ?? {} }
|
|
1828
1853
|
};
|
|
1829
1854
|
}
|
|
1855
|
+
function mergeQualityGate(base, override) {
|
|
1856
|
+
if (!override) return base;
|
|
1857
|
+
return {
|
|
1858
|
+
enabled: override.enabled ?? base.enabled,
|
|
1859
|
+
algorithm: override.algorithm ?? base.algorithm,
|
|
1860
|
+
algorithms: { ...base.algorithms, ...override.algorithms ?? {} }
|
|
1861
|
+
};
|
|
1862
|
+
}
|
|
1830
1863
|
function mergeLayer(config, data) {
|
|
1831
1864
|
return {
|
|
1832
1865
|
enabled: data.enabled ?? config.enabled,
|
|
@@ -1846,7 +1879,8 @@ function mergeLayer(config, data) {
|
|
|
1846
1879
|
],
|
|
1847
1880
|
compress: mergeCompress(config.compress, data.compress),
|
|
1848
1881
|
gc: mergeGC(config.gc, data.gc),
|
|
1849
|
-
strategies: mergeStrategies(config.strategies, data.strategies)
|
|
1882
|
+
strategies: mergeStrategies(config.strategies, data.strategies),
|
|
1883
|
+
qualityGate: mergeQualityGate(config.qualityGate, data.qualityGate)
|
|
1850
1884
|
};
|
|
1851
1885
|
}
|
|
1852
1886
|
function scheduleParseWarning(ctx, title, message) {
|
|
@@ -2131,20 +2165,27 @@ function countMessageCharacters(msg) {
|
|
|
2131
2165
|
|
|
2132
2166
|
// lib/prompts/extensions/tool.ts
|
|
2133
2167
|
var RANGE_FORMAT_EXTENSION = `
|
|
2168
|
+
|
|
2134
2169
|
THE FORMAT OF COMPRESS
|
|
2135
2170
|
|
|
2136
2171
|
\`\`\`
|
|
2137
2172
|
{
|
|
2138
|
-
topic
|
|
2173
|
+
topic?: string, // OPTIONAL fallback topic for entries without their own.
|
|
2174
|
+
// Omit when every content entry specifies its own topic.
|
|
2139
2175
|
content: [ // One or more ranges to compress
|
|
2140
2176
|
{
|
|
2177
|
+
topic?: string, // OPTIONAL per-entry topic for this range.
|
|
2178
|
+
// Falls back to top-level topic.
|
|
2179
|
+
// Give each entry its own topic when compressing
|
|
2180
|
+
// unrelated ranges in one call.
|
|
2141
2181
|
startId: string, // Boundary ID at range start: mNNNNN or bN
|
|
2142
2182
|
endId: string, // Boundary ID at range end: mNNNNN or bN
|
|
2143
2183
|
summary: string // Complete technical summary replacing all content in range
|
|
2144
2184
|
}
|
|
2145
2185
|
]
|
|
2146
2186
|
}
|
|
2147
|
-
|
|
2187
|
+
\`\`\`
|
|
2188
|
+
Each entry needs a topic \u2014 either its own or the top-level fallback.`;
|
|
2148
2189
|
var MESSAGE_FORMAT_EXTENSION = `
|
|
2149
2190
|
THE FORMAT OF COMPRESS
|
|
2150
2191
|
|
|
@@ -3279,6 +3320,8 @@ var SoftIssue = class extends Error {
|
|
|
3279
3320
|
this.kind = kind;
|
|
3280
3321
|
this.messageId = messageId;
|
|
3281
3322
|
}
|
|
3323
|
+
kind;
|
|
3324
|
+
messageId;
|
|
3282
3325
|
};
|
|
3283
3326
|
function validateArgs(args) {
|
|
3284
3327
|
if (typeof args.topic !== "string" || args.topic.trim().length === 0) {
|
|
@@ -3969,9 +4012,7 @@ function applyPendingCompressionDurations(state) {
|
|
|
3969
4012
|
// lib/compress/range-utils.ts
|
|
3970
4013
|
var BLOCK_PLACEHOLDER_REGEX = /\(b(\d+)\)|\{block_(\d+)\}/gi;
|
|
3971
4014
|
function validateArgs2(args) {
|
|
3972
|
-
|
|
3973
|
-
throw new Error("topic is required and must be a non-empty string");
|
|
3974
|
-
}
|
|
4015
|
+
const hasTopLevelTopic = typeof args.topic === "string" && args.topic.trim().length > 0;
|
|
3975
4016
|
if (!Array.isArray(args.content) || args.content.length === 0) {
|
|
3976
4017
|
throw new Error("content is required and must be a non-empty array");
|
|
3977
4018
|
}
|
|
@@ -3987,11 +4028,18 @@ function validateArgs2(args) {
|
|
|
3987
4028
|
if (typeof entry?.summary !== "string" || entry.summary.trim().length === 0) {
|
|
3988
4029
|
throw new Error(`${prefix}.summary is required and must be a non-empty string`);
|
|
3989
4030
|
}
|
|
4031
|
+
const hasEntryTopic = typeof entry?.topic === "string" && entry.topic.trim().length > 0;
|
|
4032
|
+
if (!hasEntryTopic && !hasTopLevelTopic) {
|
|
4033
|
+
throw new Error(
|
|
4034
|
+
`${prefix} needs a topic \u2014 provide ${prefix}.topic or the top-level topic`
|
|
4035
|
+
);
|
|
4036
|
+
}
|
|
3990
4037
|
}
|
|
3991
4038
|
}
|
|
3992
4039
|
function resolveRanges(args, searchContext, state) {
|
|
3993
4040
|
return args.content.map((entry, index) => {
|
|
3994
4041
|
const normalizedEntry = {
|
|
4042
|
+
topic: typeof entry.topic === "string" && entry.topic.trim().length > 0 ? entry.topic.trim() : void 0,
|
|
3995
4043
|
startId: entry.startId.trim(),
|
|
3996
4044
|
endId: entry.endId.trim(),
|
|
3997
4045
|
summary: entry.summary
|
|
@@ -4185,8 +4233,8 @@ function rebuildRangeInvocation(state, input, searchContext, invocation, protect
|
|
|
4185
4233
|
applyCompressionState(
|
|
4186
4234
|
state,
|
|
4187
4235
|
{
|
|
4188
|
-
topic: input.topic,
|
|
4189
|
-
batchTopic: input.topic,
|
|
4236
|
+
topic: plan.entry.topic ?? input.topic ?? "",
|
|
4237
|
+
batchTopic: typeof input.topic === "string" ? input.topic : void 0,
|
|
4190
4238
|
startId: plan.entry.startId,
|
|
4191
4239
|
endId: plan.entry.endId,
|
|
4192
4240
|
mode: "range",
|
|
@@ -4253,7 +4301,7 @@ function rebuildMessageInvocation(state, input, searchContext, invocation, gcCon
|
|
|
4253
4301
|
state,
|
|
4254
4302
|
{
|
|
4255
4303
|
topic: entry.topic,
|
|
4256
|
-
batchTopic: input.topic,
|
|
4304
|
+
batchTopic: typeof input.topic === "string" ? input.topic : void 0,
|
|
4257
4305
|
startId: entry.messageId,
|
|
4258
4306
|
endId: entry.messageId,
|
|
4259
4307
|
mode: "message",
|
|
@@ -5076,7 +5124,8 @@ async function sendCompressNotification(client, logger, config, state, sessionId
|
|
|
5076
5124
|
newlyCompressedToolIds.push(toolId);
|
|
5077
5125
|
}
|
|
5078
5126
|
}
|
|
5079
|
-
const
|
|
5127
|
+
const entryBlockTopics = entries.map((e) => state.prune.messages.blocksById.get(e.blockId)?.topic).filter((t) => typeof t === "string" && t.length > 0);
|
|
5128
|
+
const topic = batchTopic ?? (entries.length === 1 ? state.prune.messages.blocksById.get(entries[0]?.blockId ?? -1)?.topic ?? "(unknown topic)" : entryBlockTopics.length > 0 ? entryBlockTopics.join(" \xB7 ") : "(unknown topic)");
|
|
5080
5129
|
const contextTokensAfter = Math.max(
|
|
5081
5130
|
0,
|
|
5082
5131
|
contextTokensBefore - compressedTokens + summaryTokens
|
|
@@ -5131,20 +5180,22 @@ ${progressBar}`;
|
|
|
5131
5180
|
\u2192 Compression (~${summaryTokensStr}): ${displaySummary}`;
|
|
5132
5181
|
}
|
|
5133
5182
|
}
|
|
5134
|
-
if (config.pruneNotificationType === "
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
title: "ACP: Compress Notification",
|
|
5140
|
-
message: toastMessage,
|
|
5141
|
-
variant: "info",
|
|
5142
|
-
duration: 5e3
|
|
5143
|
-
}
|
|
5144
|
-
});
|
|
5145
|
-
return true;
|
|
5183
|
+
if (config.pruneNotificationType === "chat") {
|
|
5184
|
+
logger.warn(
|
|
5185
|
+
"compress.pruneNotificationType 'chat' is no longer supported (it injects an empty user message that causes provider 400 errors); falling back to toast. Set pruneNotificationType to 'toast' (or pruneNotification to 'off') to silence this warning.",
|
|
5186
|
+
{ sessionId }
|
|
5187
|
+
);
|
|
5146
5188
|
}
|
|
5147
|
-
|
|
5189
|
+
let toastMessage = message;
|
|
5190
|
+
toastMessage = config.pruneNotification === "minimal" ? toastMessage : truncateToastBody(toastMessage);
|
|
5191
|
+
await client.tui.showToast({
|
|
5192
|
+
body: {
|
|
5193
|
+
title: "ACP: Compress Notification",
|
|
5194
|
+
message: toastMessage,
|
|
5195
|
+
variant: "info",
|
|
5196
|
+
duration: 5e3
|
|
5197
|
+
}
|
|
5198
|
+
});
|
|
5148
5199
|
return true;
|
|
5149
5200
|
}
|
|
5150
5201
|
async function sendIgnoredMessage(client, sessionID, text, params, logger) {
|
|
@@ -5178,6 +5229,737 @@ async function sendIgnoredMessage(client, sessionID, text, params, logger) {
|
|
|
5178
5229
|
}
|
|
5179
5230
|
}
|
|
5180
5231
|
|
|
5232
|
+
// lib/compress/quality-gate/registry.ts
|
|
5233
|
+
var registry = /* @__PURE__ */ new Map();
|
|
5234
|
+
function registerQualityGate(gate) {
|
|
5235
|
+
if (registry.has(gate.name)) {
|
|
5236
|
+
const existing = registry.get(gate.name);
|
|
5237
|
+
if (existing !== gate && existing.version !== gate.version) {
|
|
5238
|
+
throw new Error(
|
|
5239
|
+
`Quality gate "${gate.name}" already registered with version ${existing.version} (attempted ${gate.version})`
|
|
5240
|
+
);
|
|
5241
|
+
}
|
|
5242
|
+
}
|
|
5243
|
+
registry.set(gate.name, gate);
|
|
5244
|
+
}
|
|
5245
|
+
function getQualityGate(name) {
|
|
5246
|
+
return registry.get(name);
|
|
5247
|
+
}
|
|
5248
|
+
|
|
5249
|
+
// node_modules/context-compress-algorithms/dist/chunk-E6LYOQFY.js
|
|
5250
|
+
var ENGLISH_WORD_RE = /[a-z][a-z0-9_]+/g;
|
|
5251
|
+
var CJK_RE = /[\u4e00-\u9fff]/g;
|
|
5252
|
+
var FILE_PATH_RE = /(?:[a-zA-Z0-9_-]+\/){1,}[a-zA-Z0-9_-]+\.[a-zA-Z]{1,5}/g;
|
|
5253
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
5254
|
+
"the",
|
|
5255
|
+
"that",
|
|
5256
|
+
"this",
|
|
5257
|
+
"these",
|
|
5258
|
+
"those",
|
|
5259
|
+
"there",
|
|
5260
|
+
"their",
|
|
5261
|
+
"them",
|
|
5262
|
+
"then",
|
|
5263
|
+
"than",
|
|
5264
|
+
"thats",
|
|
5265
|
+
"with",
|
|
5266
|
+
"will",
|
|
5267
|
+
"would",
|
|
5268
|
+
"could",
|
|
5269
|
+
"should",
|
|
5270
|
+
"from",
|
|
5271
|
+
"have",
|
|
5272
|
+
"has",
|
|
5273
|
+
"had",
|
|
5274
|
+
"having",
|
|
5275
|
+
"were",
|
|
5276
|
+
"what",
|
|
5277
|
+
"which",
|
|
5278
|
+
"when",
|
|
5279
|
+
"where",
|
|
5280
|
+
"while",
|
|
5281
|
+
"your",
|
|
5282
|
+
"yours",
|
|
5283
|
+
"theirs",
|
|
5284
|
+
"ours",
|
|
5285
|
+
"mine",
|
|
5286
|
+
"hers",
|
|
5287
|
+
"whose",
|
|
5288
|
+
"they",
|
|
5289
|
+
"them",
|
|
5290
|
+
"those",
|
|
5291
|
+
"these",
|
|
5292
|
+
"this",
|
|
5293
|
+
"that",
|
|
5294
|
+
"such",
|
|
5295
|
+
"some",
|
|
5296
|
+
"same",
|
|
5297
|
+
"other",
|
|
5298
|
+
"another",
|
|
5299
|
+
"each",
|
|
5300
|
+
"into",
|
|
5301
|
+
"onto",
|
|
5302
|
+
"upon",
|
|
5303
|
+
"over",
|
|
5304
|
+
"under",
|
|
5305
|
+
"between",
|
|
5306
|
+
"through",
|
|
5307
|
+
"during",
|
|
5308
|
+
"before",
|
|
5309
|
+
"after",
|
|
5310
|
+
"above",
|
|
5311
|
+
"below",
|
|
5312
|
+
"among",
|
|
5313
|
+
"across",
|
|
5314
|
+
"along",
|
|
5315
|
+
"around",
|
|
5316
|
+
"about",
|
|
5317
|
+
"because",
|
|
5318
|
+
"since",
|
|
5319
|
+
"unless",
|
|
5320
|
+
"although",
|
|
5321
|
+
"though",
|
|
5322
|
+
"whereas",
|
|
5323
|
+
"whether",
|
|
5324
|
+
"either",
|
|
5325
|
+
"neither",
|
|
5326
|
+
"both",
|
|
5327
|
+
"also",
|
|
5328
|
+
"only",
|
|
5329
|
+
"just",
|
|
5330
|
+
"very",
|
|
5331
|
+
"more",
|
|
5332
|
+
"most",
|
|
5333
|
+
"much",
|
|
5334
|
+
"many",
|
|
5335
|
+
"less",
|
|
5336
|
+
"least",
|
|
5337
|
+
"several",
|
|
5338
|
+
"enough",
|
|
5339
|
+
"make",
|
|
5340
|
+
"made",
|
|
5341
|
+
"makes",
|
|
5342
|
+
"making",
|
|
5343
|
+
"take",
|
|
5344
|
+
"took",
|
|
5345
|
+
"taken",
|
|
5346
|
+
"takes",
|
|
5347
|
+
"taking",
|
|
5348
|
+
"get",
|
|
5349
|
+
"got",
|
|
5350
|
+
"getting",
|
|
5351
|
+
"gets",
|
|
5352
|
+
"give",
|
|
5353
|
+
"gave",
|
|
5354
|
+
"given",
|
|
5355
|
+
"gives",
|
|
5356
|
+
"giving",
|
|
5357
|
+
"come",
|
|
5358
|
+
"came",
|
|
5359
|
+
"comes",
|
|
5360
|
+
"coming",
|
|
5361
|
+
"were",
|
|
5362
|
+
"been",
|
|
5363
|
+
"being",
|
|
5364
|
+
"have",
|
|
5365
|
+
"make",
|
|
5366
|
+
"want",
|
|
5367
|
+
"need",
|
|
5368
|
+
"using",
|
|
5369
|
+
"true",
|
|
5370
|
+
"false",
|
|
5371
|
+
"null",
|
|
5372
|
+
"undefined",
|
|
5373
|
+
"void",
|
|
5374
|
+
"return",
|
|
5375
|
+
"returns",
|
|
5376
|
+
"returning",
|
|
5377
|
+
"function",
|
|
5378
|
+
"const",
|
|
5379
|
+
"class",
|
|
5380
|
+
"interface",
|
|
5381
|
+
"type",
|
|
5382
|
+
"typeof",
|
|
5383
|
+
"instanceof",
|
|
5384
|
+
"import",
|
|
5385
|
+
"export",
|
|
5386
|
+
"require",
|
|
5387
|
+
"module",
|
|
5388
|
+
"default",
|
|
5389
|
+
"async",
|
|
5390
|
+
"await",
|
|
5391
|
+
"static",
|
|
5392
|
+
"public",
|
|
5393
|
+
"private",
|
|
5394
|
+
"protected",
|
|
5395
|
+
"readonly",
|
|
5396
|
+
"partial",
|
|
5397
|
+
"abstract",
|
|
5398
|
+
"virtual",
|
|
5399
|
+
"override",
|
|
5400
|
+
"final",
|
|
5401
|
+
"super",
|
|
5402
|
+
"value",
|
|
5403
|
+
"values",
|
|
5404
|
+
"param",
|
|
5405
|
+
"params",
|
|
5406
|
+
"name",
|
|
5407
|
+
"names",
|
|
5408
|
+
"test",
|
|
5409
|
+
"tests",
|
|
5410
|
+
"testing",
|
|
5411
|
+
"tested",
|
|
5412
|
+
"expect",
|
|
5413
|
+
"expected",
|
|
5414
|
+
"actual",
|
|
5415
|
+
"actuals",
|
|
5416
|
+
"result",
|
|
5417
|
+
"results",
|
|
5418
|
+
"output",
|
|
5419
|
+
"outputs",
|
|
5420
|
+
"input",
|
|
5421
|
+
"inputs",
|
|
5422
|
+
"data",
|
|
5423
|
+
"record",
|
|
5424
|
+
"records",
|
|
5425
|
+
"item",
|
|
5426
|
+
"items",
|
|
5427
|
+
"like",
|
|
5428
|
+
"want",
|
|
5429
|
+
"need",
|
|
5430
|
+
"used",
|
|
5431
|
+
"uses",
|
|
5432
|
+
"said",
|
|
5433
|
+
"say",
|
|
5434
|
+
"says",
|
|
5435
|
+
"went",
|
|
5436
|
+
"goes",
|
|
5437
|
+
"here",
|
|
5438
|
+
"there",
|
|
5439
|
+
"where",
|
|
5440
|
+
"when",
|
|
5441
|
+
"what",
|
|
5442
|
+
"who",
|
|
5443
|
+
"how",
|
|
5444
|
+
"why",
|
|
5445
|
+
"which",
|
|
5446
|
+
"whose",
|
|
5447
|
+
"whom",
|
|
5448
|
+
"yourself",
|
|
5449
|
+
"myself",
|
|
5450
|
+
"itself",
|
|
5451
|
+
"themselves",
|
|
5452
|
+
"ourselves",
|
|
5453
|
+
"himself",
|
|
5454
|
+
"herself",
|
|
5455
|
+
"yeah",
|
|
5456
|
+
"okay",
|
|
5457
|
+
"ok",
|
|
5458
|
+
"yes",
|
|
5459
|
+
"no",
|
|
5460
|
+
"not",
|
|
5461
|
+
"nor",
|
|
5462
|
+
"or",
|
|
5463
|
+
"and",
|
|
5464
|
+
"but",
|
|
5465
|
+
"if",
|
|
5466
|
+
"then",
|
|
5467
|
+
"else",
|
|
5468
|
+
"elif",
|
|
5469
|
+
"when",
|
|
5470
|
+
"while",
|
|
5471
|
+
"for",
|
|
5472
|
+
"to",
|
|
5473
|
+
"of",
|
|
5474
|
+
"in",
|
|
5475
|
+
"on",
|
|
5476
|
+
"at",
|
|
5477
|
+
"by",
|
|
5478
|
+
"with",
|
|
5479
|
+
"from",
|
|
5480
|
+
"into",
|
|
5481
|
+
"onto",
|
|
5482
|
+
"\u7684",
|
|
5483
|
+
"\u662F",
|
|
5484
|
+
"\u4E86",
|
|
5485
|
+
"\u5728",
|
|
5486
|
+
"\u548C",
|
|
5487
|
+
"\u4E0E",
|
|
5488
|
+
"\u6216",
|
|
5489
|
+
"\u4E5F",
|
|
5490
|
+
"\u90FD",
|
|
5491
|
+
"\u5C31",
|
|
5492
|
+
"\u8FD8",
|
|
5493
|
+
"\u53C8",
|
|
5494
|
+
"\u624D",
|
|
5495
|
+
"\u518D",
|
|
5496
|
+
"\u5DF2",
|
|
5497
|
+
"\u5C06",
|
|
5498
|
+
"\u4F1A",
|
|
5499
|
+
"\u80FD",
|
|
5500
|
+
"\u53EF",
|
|
5501
|
+
"\u53EF\u4EE5",
|
|
5502
|
+
"\u8981",
|
|
5503
|
+
"\u60F3",
|
|
5504
|
+
"\u9700\u8981",
|
|
5505
|
+
"\u5E94\u8BE5",
|
|
5506
|
+
"\u5FC5\u987B",
|
|
5507
|
+
"\u6CA1",
|
|
5508
|
+
"\u6CA1\u6709",
|
|
5509
|
+
"\u4E0D",
|
|
5510
|
+
"\u975E",
|
|
5511
|
+
"\u65E0",
|
|
5512
|
+
"\u83AB",
|
|
5513
|
+
"\u6211",
|
|
5514
|
+
"\u4F60",
|
|
5515
|
+
"\u4ED6",
|
|
5516
|
+
"\u5979",
|
|
5517
|
+
"\u5B83",
|
|
5518
|
+
"\u6211\u4EEC",
|
|
5519
|
+
"\u4F60\u4EEC",
|
|
5520
|
+
"\u4ED6\u4EEC",
|
|
5521
|
+
"\u5979\u4EEC",
|
|
5522
|
+
"\u5B83\u4EEC",
|
|
5523
|
+
"\u54B1",
|
|
5524
|
+
"\u54B1\u4EEC",
|
|
5525
|
+
"\u81EA\u5DF1",
|
|
5526
|
+
"\u8FD9",
|
|
5527
|
+
"\u90A3",
|
|
5528
|
+
"\u8FD9\u4E2A",
|
|
5529
|
+
"\u90A3\u4E2A",
|
|
5530
|
+
"\u8FD9\u4E9B",
|
|
5531
|
+
"\u90A3\u4E9B",
|
|
5532
|
+
"\u8FD9\u6837",
|
|
5533
|
+
"\u90A3\u6837",
|
|
5534
|
+
"\u8FD9\u91CC",
|
|
5535
|
+
"\u90A3\u91CC",
|
|
5536
|
+
"\u8FD9\u4E48",
|
|
5537
|
+
"\u90A3\u4E48",
|
|
5538
|
+
"\u4EC0\u4E48",
|
|
5539
|
+
"\u600E\u4E48",
|
|
5540
|
+
"\u4E3A\u4EC0\u4E48",
|
|
5541
|
+
"\u54EA",
|
|
5542
|
+
"\u54EA\u4E2A",
|
|
5543
|
+
"\u54EA\u4E9B",
|
|
5544
|
+
"\u54EA\u91CC",
|
|
5545
|
+
"\u600E\u6837",
|
|
5546
|
+
"\u591A\u5C11",
|
|
5547
|
+
"\u51E0",
|
|
5548
|
+
"\u591A",
|
|
5549
|
+
"\u5C11",
|
|
5550
|
+
"\u4E8E",
|
|
5551
|
+
"\u4ECE",
|
|
5552
|
+
"\u5411",
|
|
5553
|
+
"\u5F80",
|
|
5554
|
+
"\u5230",
|
|
5555
|
+
"\u81F3",
|
|
5556
|
+
"\u4E3A",
|
|
5557
|
+
"\u5BF9\u4E8E",
|
|
5558
|
+
"\u5173\u4E8E",
|
|
5559
|
+
"\u81F3\u4E8E",
|
|
5560
|
+
"\u7531\u4E8E",
|
|
5561
|
+
"\u56E0\u4E3A",
|
|
5562
|
+
"\u6240\u4EE5",
|
|
5563
|
+
"\u4F46\u662F",
|
|
5564
|
+
"\u4F46",
|
|
5565
|
+
"\u4E0D\u8FC7",
|
|
5566
|
+
"\u7136\u800C",
|
|
5567
|
+
"\u53EF\u662F",
|
|
5568
|
+
"\u53EA\u662F",
|
|
5569
|
+
"\u53EA\u6709",
|
|
5570
|
+
"\u9664\u4E86",
|
|
5571
|
+
"\u9664\u975E",
|
|
5572
|
+
"\u65E0\u8BBA",
|
|
5573
|
+
"\u4E0D\u7BA1",
|
|
5574
|
+
"\u5C3D\u7BA1",
|
|
5575
|
+
"\u867D\u7136",
|
|
5576
|
+
"\u867D\u8BF4",
|
|
5577
|
+
"\u5373\u4F7F",
|
|
5578
|
+
"\u5373\u4FBF",
|
|
5579
|
+
"\u54EA\u6015",
|
|
5580
|
+
"\u4E00\u65E6",
|
|
5581
|
+
"\u5982\u679C",
|
|
5582
|
+
"\u8981\u662F",
|
|
5583
|
+
"\u5047\u5982",
|
|
5584
|
+
"\u5047\u4F7F",
|
|
5585
|
+
"\u5018\u82E5",
|
|
5586
|
+
"\u4E4B",
|
|
5587
|
+
"\u5176",
|
|
5588
|
+
"\u5176\u4E2D",
|
|
5589
|
+
"\u5176\u4ED6",
|
|
5590
|
+
"\u5176\u5B83",
|
|
5591
|
+
"\u5176\u4F59",
|
|
5592
|
+
"\u53E6\u4E00",
|
|
5593
|
+
"\u53E6\u5916",
|
|
5594
|
+
"\u6B64\u5916",
|
|
5595
|
+
"\u5E76\u4E14",
|
|
5596
|
+
"\u5E76",
|
|
5597
|
+
"\u4E14",
|
|
5598
|
+
"\u7740",
|
|
5599
|
+
"\u8FC7",
|
|
5600
|
+
"\u5427",
|
|
5601
|
+
"\u5417",
|
|
5602
|
+
"\u5462",
|
|
5603
|
+
"\u554A",
|
|
5604
|
+
"\u54E6",
|
|
5605
|
+
"\u55EF",
|
|
5606
|
+
"\u5440",
|
|
5607
|
+
"\u54C7",
|
|
5608
|
+
"\u54C8",
|
|
5609
|
+
"\u561B",
|
|
5610
|
+
"\u54AF",
|
|
5611
|
+
"\u54DF"
|
|
5612
|
+
]);
|
|
5613
|
+
var ZH_STOPWORD_BIGRAMS = /* @__PURE__ */ new Set([
|
|
5614
|
+
"\u6211\u4EEC",
|
|
5615
|
+
"\u4F60\u4EEC",
|
|
5616
|
+
"\u4ED6\u4EEC",
|
|
5617
|
+
"\u5979\u4EEC",
|
|
5618
|
+
"\u5B83\u4EEC",
|
|
5619
|
+
"\u54B1\u4EEC",
|
|
5620
|
+
"\u8FD9\u4E2A",
|
|
5621
|
+
"\u90A3\u4E2A",
|
|
5622
|
+
"\u8FD9\u4E9B",
|
|
5623
|
+
"\u90A3\u4E9B",
|
|
5624
|
+
"\u4EC0\u4E48",
|
|
5625
|
+
"\u600E\u4E48",
|
|
5626
|
+
"\u4E3A\u4EC0\u4E48",
|
|
5627
|
+
"\u5982\u4F55",
|
|
5628
|
+
"\u53EF\u4EE5",
|
|
5629
|
+
"\u5E94\u8BE5",
|
|
5630
|
+
"\u4F46\u662F",
|
|
5631
|
+
"\u56E0\u4E3A",
|
|
5632
|
+
"\u6240\u4EE5",
|
|
5633
|
+
"\u5982\u679C",
|
|
5634
|
+
"\u867D\u7136",
|
|
5635
|
+
"\u5373\u4F7F",
|
|
5636
|
+
"\u5C3D\u7BA1",
|
|
5637
|
+
"\u4E3A\u4E86",
|
|
5638
|
+
"\u7531\u4E8E",
|
|
5639
|
+
"\u4E0D\u4F46",
|
|
5640
|
+
"\u800C\u4E14",
|
|
5641
|
+
"\u5E76\u4E14",
|
|
5642
|
+
"\u6216\u8005",
|
|
5643
|
+
"\u8FD8\u662F",
|
|
5644
|
+
"\u4EE5\u53CA",
|
|
5645
|
+
"\u4EE5\u4E3A",
|
|
5646
|
+
"\u4E8E\u662F",
|
|
5647
|
+
"\u7136\u800C",
|
|
5648
|
+
"\u5176\u5B9E",
|
|
5649
|
+
"\u5C31\u662F",
|
|
5650
|
+
"\u53EA\u662F",
|
|
5651
|
+
"\u53EA\u6709",
|
|
5652
|
+
"\u9664\u4E86",
|
|
5653
|
+
"\u9664\u975E",
|
|
5654
|
+
"\u8FD9\u6837",
|
|
5655
|
+
"\u90A3\u6837",
|
|
5656
|
+
"\u8FD9\u4E48",
|
|
5657
|
+
"\u90A3\u4E48",
|
|
5658
|
+
"\u8FD9\u4E9B",
|
|
5659
|
+
"\u90A3\u4E9B",
|
|
5660
|
+
"\u8FD9\u91CC",
|
|
5661
|
+
"\u90A3\u91CC",
|
|
5662
|
+
"\u73B0\u5728",
|
|
5663
|
+
"\u4EE5\u540E",
|
|
5664
|
+
"\u4EE5\u524D",
|
|
5665
|
+
"\u4E4B\u540E",
|
|
5666
|
+
"\u4E4B\u524D",
|
|
5667
|
+
"\u7136\u540E",
|
|
5668
|
+
"\u5F53\u7136",
|
|
5669
|
+
"\u53EF\u80FD",
|
|
5670
|
+
"\u4E00\u4E9B",
|
|
5671
|
+
"\u8BB8\u591A",
|
|
5672
|
+
"\u975E\u5E38",
|
|
5673
|
+
"\u5341\u5206",
|
|
5674
|
+
"\u6BD4\u8F83",
|
|
5675
|
+
"\u66F4\u52A0",
|
|
5676
|
+
"\u6700\u4E3A",
|
|
5677
|
+
"\u4E5F\u662F",
|
|
5678
|
+
"\u8FD8\u662F",
|
|
5679
|
+
"\u5C31\u662F",
|
|
5680
|
+
"\u4E0D\u8FC7",
|
|
5681
|
+
"\u4E0D\u8981",
|
|
5682
|
+
"\u4E0D\u80FD",
|
|
5683
|
+
"\u4E0D\u4F1A",
|
|
5684
|
+
"\u6CA1\u6709",
|
|
5685
|
+
"\u4E0D\u662F",
|
|
5686
|
+
"\u4E0D\u7528",
|
|
5687
|
+
"\u4E0D\u5FC5",
|
|
5688
|
+
"\u4E00\u76F4",
|
|
5689
|
+
"\u5DF2\u7ECF",
|
|
5690
|
+
"\u6B63\u5728",
|
|
5691
|
+
"\u9A6C\u4E0A"
|
|
5692
|
+
]);
|
|
5693
|
+
var DEFAULT_OPTS = {
|
|
5694
|
+
english: true,
|
|
5695
|
+
zhUnigrams: true,
|
|
5696
|
+
zhBigrams: true
|
|
5697
|
+
};
|
|
5698
|
+
function tokenize(text, opts = {}) {
|
|
5699
|
+
if (!text || typeof text !== "string") return [];
|
|
5700
|
+
const options = { ...DEFAULT_OPTS, ...opts };
|
|
5701
|
+
const tokens = [];
|
|
5702
|
+
const lower = text.toLowerCase();
|
|
5703
|
+
if (options.english) {
|
|
5704
|
+
const matches = lower.match(ENGLISH_WORD_RE);
|
|
5705
|
+
if (matches) {
|
|
5706
|
+
for (const w of matches) {
|
|
5707
|
+
if (w.length >= 4 && !STOPWORDS.has(w) && !/^\d+$/.test(w)) {
|
|
5708
|
+
tokens.push(w);
|
|
5709
|
+
}
|
|
5710
|
+
}
|
|
5711
|
+
}
|
|
5712
|
+
}
|
|
5713
|
+
if (options.zhUnigrams || options.zhBigrams) {
|
|
5714
|
+
const cjkChars = text.match(CJK_RE);
|
|
5715
|
+
if (cjkChars && cjkChars.length > 0) {
|
|
5716
|
+
const cjkStr = cjkChars.join("");
|
|
5717
|
+
if (options.zhUnigrams) {
|
|
5718
|
+
for (const c of cjkStr) {
|
|
5719
|
+
if (!STOPWORDS.has(c)) tokens.push(c);
|
|
5720
|
+
}
|
|
5721
|
+
}
|
|
5722
|
+
if (options.zhBigrams) {
|
|
5723
|
+
for (let i = 0; i < cjkStr.length - 1; i++) {
|
|
5724
|
+
const bg = cjkStr.slice(i, i + 2);
|
|
5725
|
+
if (!ZH_STOPWORD_BIGRAMS.has(bg) && !STOPWORDS.has(bg[0]) && !STOPWORDS.has(bg[1])) {
|
|
5726
|
+
tokens.push(bg);
|
|
5727
|
+
}
|
|
5728
|
+
}
|
|
5729
|
+
}
|
|
5730
|
+
}
|
|
5731
|
+
}
|
|
5732
|
+
return tokens;
|
|
5733
|
+
}
|
|
5734
|
+
function termFrequency(tokens) {
|
|
5735
|
+
const tf = /* @__PURE__ */ new Map();
|
|
5736
|
+
for (const t of tokens) {
|
|
5737
|
+
tf.set(t, (tf.get(t) ?? 0) + 1);
|
|
5738
|
+
}
|
|
5739
|
+
return tf;
|
|
5740
|
+
}
|
|
5741
|
+
function topKByTf(tokens, k) {
|
|
5742
|
+
if (tokens.length === 0 || k <= 0) return [];
|
|
5743
|
+
const tf = termFrequency(tokens);
|
|
5744
|
+
const sorted = [...tf.entries()].sort((a, b) => {
|
|
5745
|
+
if (b[1] !== a[1]) return b[1] - a[1];
|
|
5746
|
+
return a[0].localeCompare(b[0]);
|
|
5747
|
+
});
|
|
5748
|
+
return sorted.slice(0, k).map((e) => e[0]);
|
|
5749
|
+
}
|
|
5750
|
+
function extractFilePaths(text) {
|
|
5751
|
+
if (!text) return /* @__PURE__ */ new Set();
|
|
5752
|
+
const paths = /* @__PURE__ */ new Set();
|
|
5753
|
+
const matches = text.match(FILE_PATH_RE);
|
|
5754
|
+
if (matches) {
|
|
5755
|
+
for (const m of matches) paths.add(m);
|
|
5756
|
+
}
|
|
5757
|
+
return paths;
|
|
5758
|
+
}
|
|
5759
|
+
function rouge1Recall(summaryTokens, originalTokens) {
|
|
5760
|
+
if (originalTokens.length === 0) return 0;
|
|
5761
|
+
const summarySet = new Set(summaryTokens);
|
|
5762
|
+
const originalSet = new Set(originalTokens);
|
|
5763
|
+
let hit = 0;
|
|
5764
|
+
for (const t of originalSet) if (summarySet.has(t)) hit++;
|
|
5765
|
+
return hit / originalSet.size;
|
|
5766
|
+
}
|
|
5767
|
+
function rouge1Precision(summaryTokens, originalTokens) {
|
|
5768
|
+
if (summaryTokens.length === 0) return 0;
|
|
5769
|
+
const summarySet = new Set(summaryTokens);
|
|
5770
|
+
const originalSet = new Set(originalTokens);
|
|
5771
|
+
let hit = 0;
|
|
5772
|
+
for (const t of summarySet) if (originalSet.has(t)) hit++;
|
|
5773
|
+
return hit / summarySet.size;
|
|
5774
|
+
}
|
|
5775
|
+
function rouge1F1(summaryTokens, originalTokens) {
|
|
5776
|
+
const r = rouge1Recall(summaryTokens, originalTokens);
|
|
5777
|
+
const p = rouge1Precision(summaryTokens, originalTokens);
|
|
5778
|
+
if (r + p === 0) return 0;
|
|
5779
|
+
return 2 * r * p / (r + p);
|
|
5780
|
+
}
|
|
5781
|
+
function topKRecall(summaryTokens, originalTokens, k) {
|
|
5782
|
+
if (originalTokens.length === 0 || k <= 0) return 0;
|
|
5783
|
+
const top = topKByTf(originalTokens, k);
|
|
5784
|
+
if (top.length === 0) return 0;
|
|
5785
|
+
const summarySet = new Set(summaryTokens);
|
|
5786
|
+
let hit = 0;
|
|
5787
|
+
for (const t of top) if (summarySet.has(t)) hit++;
|
|
5788
|
+
return hit / top.length;
|
|
5789
|
+
}
|
|
5790
|
+
var DEFAULT_ROUGE_RECALL_V1_CONFIG = {
|
|
5791
|
+
layer1MinChars: 200,
|
|
5792
|
+
layer1MinRetentionPct: 1,
|
|
5793
|
+
layer2MaxRougeF1: 0.05,
|
|
5794
|
+
layer2MaxTop20Recall: 0.2
|
|
5795
|
+
};
|
|
5796
|
+
var TOP_K = 20;
|
|
5797
|
+
var ORIGINAL_TOKEN_ESTIMATE_CHARS_PER_TOKEN = 4;
|
|
5798
|
+
function resolveConfig(input) {
|
|
5799
|
+
if (!input || typeof input !== "object") return DEFAULT_ROUGE_RECALL_V1_CONFIG;
|
|
5800
|
+
const c = input;
|
|
5801
|
+
return {
|
|
5802
|
+
layer1MinChars: typeof c.layer1MinChars === "number" && c.layer1MinChars > 0 ? c.layer1MinChars : DEFAULT_ROUGE_RECALL_V1_CONFIG.layer1MinChars,
|
|
5803
|
+
layer1MinRetentionPct: typeof c.layer1MinRetentionPct === "number" && c.layer1MinRetentionPct >= 0 ? c.layer1MinRetentionPct : DEFAULT_ROUGE_RECALL_V1_CONFIG.layer1MinRetentionPct,
|
|
5804
|
+
layer2MaxRougeF1: typeof c.layer2MaxRougeF1 === "number" && c.layer2MaxRougeF1 >= 0 ? c.layer2MaxRougeF1 : DEFAULT_ROUGE_RECALL_V1_CONFIG.layer2MaxRougeF1,
|
|
5805
|
+
layer2MaxTop20Recall: typeof c.layer2MaxTop20Recall === "number" && c.layer2MaxTop20Recall >= 0 ? c.layer2MaxTop20Recall : DEFAULT_ROUGE_RECALL_V1_CONFIG.layer2MaxTop20Recall
|
|
5806
|
+
};
|
|
5807
|
+
}
|
|
5808
|
+
var rougeRecallV1 = {
|
|
5809
|
+
name: "rouge-recall-v1",
|
|
5810
|
+
version: "1.0.0",
|
|
5811
|
+
description: "Two-layer gate: length floor (L1) then ROUGE-1 F1 AND top-20 keyword recall (L2)",
|
|
5812
|
+
evaluate(ctx, rawConfig) {
|
|
5813
|
+
const cfg = resolveConfig(rawConfig);
|
|
5814
|
+
const summaryLen = ctx.summary.length;
|
|
5815
|
+
const originalChars = ctx.originalText.length;
|
|
5816
|
+
const retentionPct = ctx.block.compressedTokens > 0 ? summaryLen / (ctx.block.compressedTokens * ORIGINAL_TOKEN_ESTIMATE_CHARS_PER_TOKEN) * 100 : 0;
|
|
5817
|
+
const baseMetrics = [
|
|
5818
|
+
{ name: "summaryLen", value: summaryLen },
|
|
5819
|
+
{ name: "retentionPct", value: +retentionPct.toFixed(2), format: "percent" },
|
|
5820
|
+
{ name: "originalTokens", value: ctx.block.compressedTokens }
|
|
5821
|
+
];
|
|
5822
|
+
if (summaryLen < cfg.layer1MinChars || originalChars > 0 && retentionPct < cfg.layer1MinRetentionPct) {
|
|
5823
|
+
return {
|
|
5824
|
+
passed: false,
|
|
5825
|
+
layer: "L1-length",
|
|
5826
|
+
reason: `Summary too short: ${summaryLen} chars, ${retentionPct.toFixed(2)}% retention (threshold: ${cfg.layer1MinChars} chars OR ${cfg.layer1MinRetentionPct}% retention)`,
|
|
5827
|
+
metrics: baseMetrics
|
|
5828
|
+
};
|
|
5829
|
+
}
|
|
5830
|
+
if (ctx.originalText.length === 0) {
|
|
5831
|
+
return { passed: true, metrics: baseMetrics };
|
|
5832
|
+
}
|
|
5833
|
+
const summaryTokens = tokenize(ctx.summary);
|
|
5834
|
+
const originalTokens = tokenize(ctx.originalText);
|
|
5835
|
+
const rougeF1 = rouge1F1(summaryTokens, originalTokens);
|
|
5836
|
+
const rougeRecall = rouge1Recall(summaryTokens, originalTokens);
|
|
5837
|
+
const top20 = topKRecall(summaryTokens, originalTokens, TOP_K);
|
|
5838
|
+
const summaryPaths = extractFilePaths(ctx.summary);
|
|
5839
|
+
const originalPaths = extractFilePaths(ctx.originalText);
|
|
5840
|
+
const pathCoverage = originalPaths.size >= 5 ? [...summaryPaths].filter((p) => originalPaths.has(p)).length / originalPaths.size : -1;
|
|
5841
|
+
const contentMetrics = [
|
|
5842
|
+
...baseMetrics,
|
|
5843
|
+
{ name: "rougeF1", value: +rougeF1.toFixed(4), format: "ratio" },
|
|
5844
|
+
{ name: "rougeRecall", value: +rougeRecall.toFixed(4), format: "ratio" },
|
|
5845
|
+
{ name: "top20Recall", value: +top20.toFixed(4), format: "ratio" },
|
|
5846
|
+
{ name: "nOriginalPaths", value: originalPaths.size },
|
|
5847
|
+
{ name: "nSummaryPaths", value: summaryPaths.size }
|
|
5848
|
+
];
|
|
5849
|
+
if (pathCoverage >= 0) {
|
|
5850
|
+
contentMetrics.push({ name: "pathCoverage", value: +pathCoverage.toFixed(3), format: "ratio" });
|
|
5851
|
+
}
|
|
5852
|
+
if (rougeF1 < cfg.layer2MaxRougeF1 && top20 < cfg.layer2MaxTop20Recall) {
|
|
5853
|
+
return {
|
|
5854
|
+
passed: false,
|
|
5855
|
+
layer: "L2-recall",
|
|
5856
|
+
reason: `Content coverage too low: rougeF1=${rougeF1.toFixed(3)}, top20Recall=${top20.toFixed(3)} (threshold: rougeF1<${cfg.layer2MaxRougeF1} AND top20<${cfg.layer2MaxTop20Recall})`,
|
|
5857
|
+
metrics: contentMetrics
|
|
5858
|
+
};
|
|
5859
|
+
}
|
|
5860
|
+
return { passed: true, metrics: contentMetrics };
|
|
5861
|
+
}
|
|
5862
|
+
};
|
|
5863
|
+
|
|
5864
|
+
// lib/compress/quality-gate/algorithms/index.ts
|
|
5865
|
+
function ensureBuiltinGatesRegistered() {
|
|
5866
|
+
registerQualityGate(rougeRecallV1);
|
|
5867
|
+
}
|
|
5868
|
+
|
|
5869
|
+
// lib/compress/quality-gate/evaluate.ts
|
|
5870
|
+
var CHARS_PER_TOKEN_ESTIMATE = 4;
|
|
5871
|
+
var TOOL_OUTPUT_MAX_CHARS = 1500;
|
|
5872
|
+
var TOOL_INPUT_MAX_CHARS = 500;
|
|
5873
|
+
function extractMessageText(parts) {
|
|
5874
|
+
if (!parts || !Array.isArray(parts)) return "";
|
|
5875
|
+
let text = "";
|
|
5876
|
+
for (const part of parts) {
|
|
5877
|
+
if (!part || typeof part !== "object") continue;
|
|
5878
|
+
if (part.type === "text") {
|
|
5879
|
+
text += part.text + "\n";
|
|
5880
|
+
} else if (part.type === "tool") {
|
|
5881
|
+
const state = part.state;
|
|
5882
|
+
const input = state.status === "completed" && typeof state.input === "object" ? JSON.stringify(state.input).slice(0, TOOL_INPUT_MAX_CHARS) : "";
|
|
5883
|
+
const output = state.status === "completed" && typeof state.output === "string" ? state.output.slice(0, TOOL_OUTPUT_MAX_CHARS) : state.status === "completed" && typeof state.output === "object" ? JSON.stringify(state.output).slice(0, TOOL_OUTPUT_MAX_CHARS) : "";
|
|
5884
|
+
text += `[tool:${part.tool}] ${input}
|
|
5885
|
+
${output}
|
|
5886
|
+
`;
|
|
5887
|
+
}
|
|
5888
|
+
}
|
|
5889
|
+
return text;
|
|
5890
|
+
}
|
|
5891
|
+
function buildContext(block, rawMessages) {
|
|
5892
|
+
const directIds = block.directMessageIds;
|
|
5893
|
+
if (!directIds || directIds.length === 0) return null;
|
|
5894
|
+
const idToMsg = /* @__PURE__ */ new Map();
|
|
5895
|
+
for (const m of rawMessages) {
|
|
5896
|
+
const id = m?.info?.id;
|
|
5897
|
+
if (typeof id === "string") idToMsg.set(id, m);
|
|
5898
|
+
}
|
|
5899
|
+
const chunks = [];
|
|
5900
|
+
for (const id of directIds) {
|
|
5901
|
+
const m = idToMsg.get(id);
|
|
5902
|
+
if (!m) continue;
|
|
5903
|
+
chunks.push(extractMessageText(m.parts));
|
|
5904
|
+
}
|
|
5905
|
+
if (chunks.length === 0) return null;
|
|
5906
|
+
const originalText = chunks.join("\n");
|
|
5907
|
+
return {
|
|
5908
|
+
block,
|
|
5909
|
+
summary: block.summary ?? "",
|
|
5910
|
+
originalChunks: chunks,
|
|
5911
|
+
originalText,
|
|
5912
|
+
originalTokens: Math.ceil(originalText.length / CHARS_PER_TOKEN_ESTIMATE)
|
|
5913
|
+
};
|
|
5914
|
+
}
|
|
5915
|
+
function evaluateBlockQuality(state, rawMessages, entry, config, logger) {
|
|
5916
|
+
const qg = config.qualityGate;
|
|
5917
|
+
if (!qg || qg.enabled !== true) return null;
|
|
5918
|
+
ensureBuiltinGatesRegistered();
|
|
5919
|
+
const algoName = qg.algorithm;
|
|
5920
|
+
if (!algoName) {
|
|
5921
|
+
logger.warn("Quality gate enabled but no algorithm specified", {});
|
|
5922
|
+
return null;
|
|
5923
|
+
}
|
|
5924
|
+
const gate = getQualityGate(algoName);
|
|
5925
|
+
if (!gate) {
|
|
5926
|
+
logger.warn("Quality gate algorithm not found in registry", { algorithm: algoName });
|
|
5927
|
+
return null;
|
|
5928
|
+
}
|
|
5929
|
+
const block = state.prune.messages.blocksById.get(entry.blockId);
|
|
5930
|
+
if (!block) {
|
|
5931
|
+
logger.warn("Quality gate: block not found", { blockId: entry.blockId });
|
|
5932
|
+
return null;
|
|
5933
|
+
}
|
|
5934
|
+
const ctx = buildContext(block, rawMessages);
|
|
5935
|
+
if (!ctx) return null;
|
|
5936
|
+
const algoConfig = (qg.algorithms && qg.algorithms[algoName]) ?? {};
|
|
5937
|
+
try {
|
|
5938
|
+
return gate.evaluate(ctx, algoConfig);
|
|
5939
|
+
} catch (err) {
|
|
5940
|
+
logger.warn("Quality gate threw \u2014 treating as pass", {
|
|
5941
|
+
gate: gate.name,
|
|
5942
|
+
blockId: entry.blockId,
|
|
5943
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5944
|
+
});
|
|
5945
|
+
return { passed: true, metrics: [] };
|
|
5946
|
+
}
|
|
5947
|
+
}
|
|
5948
|
+
function evaluateBatchQuality(state, rawMessages, entries, config, logger) {
|
|
5949
|
+
const failures = [];
|
|
5950
|
+
for (const entry of entries) {
|
|
5951
|
+
const result = evaluateBlockQuality(state, rawMessages, entry, config, logger);
|
|
5952
|
+
if (result && !result.passed) {
|
|
5953
|
+
failures.push({ blockId: entry.blockId, result });
|
|
5954
|
+
}
|
|
5955
|
+
}
|
|
5956
|
+
return {
|
|
5957
|
+
total: entries.length,
|
|
5958
|
+
passed: entries.length - failures.length,
|
|
5959
|
+
failures
|
|
5960
|
+
};
|
|
5961
|
+
}
|
|
5962
|
+
|
|
5181
5963
|
// lib/compress/pipeline.ts
|
|
5182
5964
|
function snapshotCompressionState(state) {
|
|
5183
5965
|
return {
|
|
@@ -5226,6 +6008,27 @@ async function finalizeSession(ctx, toolCtx, rawMessages, entries, batchTopic) {
|
|
|
5226
6008
|
ctx.state.manualMode = ctx.state.manualMode ? "active" : false;
|
|
5227
6009
|
applyPendingCompressionDurations(ctx.state);
|
|
5228
6010
|
await saveSessionState(ctx.state, ctx.logger);
|
|
6011
|
+
if (entries.length > 0) {
|
|
6012
|
+
const qualityReport = evaluateBatchQuality(
|
|
6013
|
+
ctx.state,
|
|
6014
|
+
rawMessages,
|
|
6015
|
+
entries,
|
|
6016
|
+
ctx.config,
|
|
6017
|
+
ctx.logger
|
|
6018
|
+
);
|
|
6019
|
+
for (const failure of qualityReport.failures) {
|
|
6020
|
+
const metrics = Object.fromEntries(
|
|
6021
|
+
failure.result.metrics.map((m) => [m.name, m.value])
|
|
6022
|
+
);
|
|
6023
|
+
ctx.logger.warn("Compression quality gate FAILED", {
|
|
6024
|
+
blockId: failure.blockId,
|
|
6025
|
+
algorithm: ctx.config.qualityGate.algorithm,
|
|
6026
|
+
layer: failure.result.layer,
|
|
6027
|
+
reason: failure.result.reason,
|
|
6028
|
+
...metrics
|
|
6029
|
+
});
|
|
6030
|
+
}
|
|
6031
|
+
}
|
|
5229
6032
|
const params = getCurrentParams(ctx.state, rawMessages, ctx.logger);
|
|
5230
6033
|
const sessionMessageIds = rawMessages.filter((msg) => !isIgnoredUserMessage(msg)).map((msg) => msg.info.id);
|
|
5231
6034
|
const contextTokensBefore = getCurrentTokenUsage(ctx.state, rawMessages);
|
|
@@ -5578,9 +6381,14 @@ function createCompressMessageTool(ctx) {
|
|
|
5578
6381
|
import { tool as tool3 } from "@opencode-ai/plugin";
|
|
5579
6382
|
function buildSchema2(maxSummaryLengthHard) {
|
|
5580
6383
|
return {
|
|
5581
|
-
topic: tool3.schema.string().
|
|
6384
|
+
topic: tool3.schema.string().optional().describe(
|
|
6385
|
+
"Fallback topic for entries without their own. Omit when each content entry specifies its own topic."
|
|
6386
|
+
),
|
|
5582
6387
|
content: tool3.schema.array(
|
|
5583
6388
|
tool3.schema.object({
|
|
6389
|
+
topic: tool3.schema.string().optional().describe(
|
|
6390
|
+
"Short label (3-5 words) for THIS range, e.g. 'Auth System Exploration'. Omit to use top-level topic. When compressing multiple unrelated ranges, give each its own topic for better quality."
|
|
6391
|
+
),
|
|
5584
6392
|
startId: tool3.schema.string().describe(
|
|
5585
6393
|
"Message or block ID marking the beginning of range (e.g. m00001, b2)"
|
|
5586
6394
|
),
|
|
@@ -5590,7 +6398,7 @@ function buildSchema2(maxSummaryLengthHard) {
|
|
|
5590
6398
|
)
|
|
5591
6399
|
})
|
|
5592
6400
|
).describe(
|
|
5593
|
-
"One or more ranges to compress, each with start/end boundaries and a summary"
|
|
6401
|
+
"One or more ranges to compress, each with start/end boundaries and a summary. When compressing multiple unrelated ranges in one call, give each its own topic."
|
|
5594
6402
|
),
|
|
5595
6403
|
summaryMaxChars: tool3.schema.number().optional().describe(
|
|
5596
6404
|
`Override max summary length (default max: ${maxSummaryLengthHard} chars). Use when content is important and needs more detail \u2014 don't lose critical info just to fit the limit.`
|
|
@@ -5624,7 +6432,7 @@ function createCompressRangeTool(ctx) {
|
|
|
5624
6432
|
const { rawMessages, searchContext } = await prepareSession(
|
|
5625
6433
|
ctx,
|
|
5626
6434
|
toolCtx,
|
|
5627
|
-
`Compress Range: ${input.topic}`
|
|
6435
|
+
`Compress Range: ${input.topic ?? "(batch)"}`
|
|
5628
6436
|
);
|
|
5629
6437
|
const resolvedPlans = resolveRanges(input, searchContext, ctx.state);
|
|
5630
6438
|
validateNonOverlapping(resolvedPlans);
|
|
@@ -5766,7 +6574,7 @@ function createCompressRangeTool(ctx) {
|
|
|
5766
6574
|
const applied = applyCompressionState(
|
|
5767
6575
|
ctx.state,
|
|
5768
6576
|
{
|
|
5769
|
-
topic: input.topic,
|
|
6577
|
+
topic: preparedPlan.entry.topic ?? input.topic ?? "",
|
|
5770
6578
|
batchTopic: input.topic,
|
|
5771
6579
|
startId: preparedPlan.entry.startId,
|
|
5772
6580
|
endId: preparedPlan.entry.endId,
|
|
@@ -5791,7 +6599,13 @@ function createCompressRangeTool(ctx) {
|
|
|
5791
6599
|
summaryTokens
|
|
5792
6600
|
});
|
|
5793
6601
|
}
|
|
5794
|
-
await finalizeSession(
|
|
6602
|
+
await finalizeSession(
|
|
6603
|
+
ctx,
|
|
6604
|
+
toolCtx,
|
|
6605
|
+
rawMessages,
|
|
6606
|
+
notifications,
|
|
6607
|
+
input.topic
|
|
6608
|
+
);
|
|
5795
6609
|
} catch (error) {
|
|
5796
6610
|
restoreCompressionState(ctx.state, snapshot);
|
|
5797
6611
|
throw error;
|
|
@@ -6195,7 +7009,7 @@ var dropEmptyMessages = (messages) => {
|
|
|
6195
7009
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
6196
7010
|
const parts = Array.isArray(messages[i].parts) ? messages[i].parts : [];
|
|
6197
7011
|
const isEmpty = parts.every(
|
|
6198
|
-
(part) => part.type === "text" && (typeof part.text !== "string" || part.text.trim().length === 0)
|
|
7012
|
+
(part) => part.type === "text" && (typeof part.text !== "string" || part.text.trim().length === 0 || part.ignored === true)
|
|
6199
7013
|
);
|
|
6200
7014
|
if (isEmpty) {
|
|
6201
7015
|
messages.splice(i, 1);
|
|
@@ -6248,7 +7062,7 @@ function buildCompressedBlockGuidance(state, gcConfig, context) {
|
|
|
6248
7062
|
}
|
|
6249
7063
|
}
|
|
6250
7064
|
const usageRatio = context?.currentTokens && context?.modelContextLimit ? context.currentTokens / context.modelContextLimit : 0;
|
|
6251
|
-
if (gcConfig && usageRatio > 0.
|
|
7065
|
+
if (gcConfig && usageRatio > 0.9) {
|
|
6252
7066
|
const promotionThreshold = gcConfig.promotionThreshold;
|
|
6253
7067
|
const agingBlocks = [];
|
|
6254
7068
|
for (const blockId of activeBlockIds) {
|
|
@@ -6266,10 +7080,10 @@ function buildCompressedBlockGuidance(state, gcConfig, context) {
|
|
|
6266
7080
|
}
|
|
6267
7081
|
if (agingBlocks.length > 0) {
|
|
6268
7082
|
lines.push("");
|
|
6269
|
-
lines.push("\u26A0\uFE0F Block aging warning \u2014 these blocks may be truncated by GC
|
|
7083
|
+
lines.push("\u26A0\uFE0F Block aging warning \u2014 context near limit, these blocks may be truncated by last-resort GC:");
|
|
6270
7084
|
lines.push(...agingBlocks);
|
|
6271
7085
|
lines.push(
|
|
6272
|
-
"
|
|
7086
|
+
"Re-summarize these blocks into concise new ones to preserve key facts. At 100% context, oversized blocks are auto-truncated as a last resort."
|
|
6273
7087
|
);
|
|
6274
7088
|
}
|
|
6275
7089
|
}
|
|
@@ -6363,6 +7177,62 @@ function listPriorityRefsBeforeIndex(messages, priorities, anchorIndex, priority
|
|
|
6363
7177
|
return refs;
|
|
6364
7178
|
}
|
|
6365
7179
|
|
|
7180
|
+
// node_modules/context-compress-algorithms/dist/chunk-UX2UW5FY.js
|
|
7181
|
+
var NUDGE_GROWTH_FLOOR = 6e3;
|
|
7182
|
+
var NUDGE_GROWTH_CAP = 5e4;
|
|
7183
|
+
var NUDGE_GROWTH_RATIO = 0.05;
|
|
7184
|
+
function computeShouldNudge(params) {
|
|
7185
|
+
const { currentTokens, overMinLimit, overMaxLimit } = params;
|
|
7186
|
+
if (currentTokens === void 0) {
|
|
7187
|
+
return { shouldNudge: false, tipsVariant: null };
|
|
7188
|
+
}
|
|
7189
|
+
if (params.lastNudgeTokens === void 0) {
|
|
7190
|
+
return { shouldNudge: false, tipsVariant: null };
|
|
7191
|
+
}
|
|
7192
|
+
const growthSinceLastNudge = currentTokens - params.lastNudgeTokens;
|
|
7193
|
+
const shouldNudge = growthSinceLastNudge >= params.nudgeGrowthTokens || overMaxLimit;
|
|
7194
|
+
if (!shouldNudge) {
|
|
7195
|
+
return { shouldNudge: false, tipsVariant: null };
|
|
7196
|
+
}
|
|
7197
|
+
const tipsVariant = overMaxLimit ? "maxLimit" : overMinLimit ? "minLimit" : "normal";
|
|
7198
|
+
return { shouldNudge: true, tipsVariant };
|
|
7199
|
+
}
|
|
7200
|
+
function resolveAdaptiveNudgeGrowth(modelContextLimit) {
|
|
7201
|
+
if (!modelContextLimit || modelContextLimit <= 0) return NUDGE_GROWTH_FLOOR;
|
|
7202
|
+
return Math.min(
|
|
7203
|
+
NUDGE_GROWTH_CAP,
|
|
7204
|
+
Math.max(NUDGE_GROWTH_FLOOR, Math.round(modelContextLimit * NUDGE_GROWTH_RATIO))
|
|
7205
|
+
);
|
|
7206
|
+
}
|
|
7207
|
+
var defaultTriggerPolicy = {
|
|
7208
|
+
name: "context-compress-algorithms-trigger",
|
|
7209
|
+
version: "1.0.0",
|
|
7210
|
+
description: "Growth-only cadence: nudge when context growth since last nudge exceeds adaptive threshold, or when over max limit.",
|
|
7211
|
+
computeShouldNudge,
|
|
7212
|
+
resolveAdaptiveNudgeGrowth
|
|
7213
|
+
};
|
|
7214
|
+
|
|
7215
|
+
// lib/messages/inject/policy/registry.ts
|
|
7216
|
+
var registry2 = /* @__PURE__ */ new Map();
|
|
7217
|
+
var defaultPolicy = null;
|
|
7218
|
+
function registerTriggerPolicy2(policy) {
|
|
7219
|
+
if (!policy.name) {
|
|
7220
|
+
throw new Error("TriggerPolicy must have a name");
|
|
7221
|
+
}
|
|
7222
|
+
registry2.set(policy.name, policy);
|
|
7223
|
+
if (!defaultPolicy) {
|
|
7224
|
+
defaultPolicy = policy;
|
|
7225
|
+
}
|
|
7226
|
+
}
|
|
7227
|
+
function getDefaultTriggerPolicy() {
|
|
7228
|
+
return defaultPolicy;
|
|
7229
|
+
}
|
|
7230
|
+
|
|
7231
|
+
// lib/messages/inject/policy/index.ts
|
|
7232
|
+
function ensureBuiltinTriggerPolicyRegistered() {
|
|
7233
|
+
registerTriggerPolicy2(defaultTriggerPolicy);
|
|
7234
|
+
}
|
|
7235
|
+
|
|
6366
7236
|
// lib/messages/inject/utils.ts
|
|
6367
7237
|
var MESSAGE_MODE_NUDGE_PRIORITY = "high";
|
|
6368
7238
|
function getNudgeFrequency(config) {
|
|
@@ -6475,31 +7345,20 @@ function isContextOverLimits(config, state, providerId, modelId, messages) {
|
|
|
6475
7345
|
modelContextLimit: state.modelContextLimit
|
|
6476
7346
|
};
|
|
6477
7347
|
}
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
}
|
|
6483
|
-
if (params.lastNudgeTokens === void 0) {
|
|
7348
|
+
ensureBuiltinTriggerPolicyRegistered();
|
|
7349
|
+
function computeShouldNudge2(params) {
|
|
7350
|
+
const policy = getDefaultTriggerPolicy();
|
|
7351
|
+
if (!policy) {
|
|
6484
7352
|
return { shouldNudge: false, tipsVariant: null };
|
|
6485
7353
|
}
|
|
6486
|
-
|
|
6487
|
-
const shouldNudge = growthSinceLastNudge >= params.nudgeGrowthTokens || overMaxLimit;
|
|
6488
|
-
if (!shouldNudge) {
|
|
6489
|
-
return { shouldNudge: false, tipsVariant: null };
|
|
6490
|
-
}
|
|
6491
|
-
const tipsVariant = overMaxLimit ? "maxLimit" : overMinLimit ? "minLimit" : "normal";
|
|
6492
|
-
return { shouldNudge: true, tipsVariant };
|
|
7354
|
+
return policy.computeShouldNudge(params);
|
|
6493
7355
|
}
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
return
|
|
6500
|
-
NUDGE_GROWTH_CAP,
|
|
6501
|
-
Math.max(NUDGE_GROWTH_FLOOR, Math.round(modelContextLimit * NUDGE_GROWTH_RATIO))
|
|
6502
|
-
);
|
|
7356
|
+
function resolveAdaptiveNudgeGrowth2(modelContextLimit) {
|
|
7357
|
+
const policy = getDefaultTriggerPolicy();
|
|
7358
|
+
if (!policy) {
|
|
7359
|
+
return 6e3;
|
|
7360
|
+
}
|
|
7361
|
+
return policy.resolveAdaptiveNudgeGrowth(modelContextLimit);
|
|
6503
7362
|
}
|
|
6504
7363
|
function addAnchor(anchorMessageIds, anchorMessageId, anchorMessageIndex, messages, interval) {
|
|
6505
7364
|
if (anchorMessageIndex < 0) {
|
|
@@ -7075,7 +7934,7 @@ ${lines2.join("\n")}`;
|
|
|
7075
7934
|
${lines.join("\n")}`;
|
|
7076
7935
|
}
|
|
7077
7936
|
|
|
7078
|
-
//
|
|
7937
|
+
// node_modules/context-compress-algorithms/dist/chunk-ZRHPFN6B.js
|
|
7079
7938
|
var COMPRESS_PHILOSOPHY = `Compression Philosophy:
|
|
7080
7939
|
- All compression serves the primary task, but be frugal.
|
|
7081
7940
|
- Context capacity is precious. Save context by compressing consumed outputs, not by avoiding tools.
|
|
@@ -7251,7 +8110,7 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
7251
8110
|
}
|
|
7252
8111
|
}
|
|
7253
8112
|
const suffixMessage = createSuffixMessage(messages);
|
|
7254
|
-
const nudgeGrowthTokens = config.compress?.nudgeGrowthTokens ??
|
|
8113
|
+
const nudgeGrowthTokens = config.compress?.nudgeGrowthTokens ?? resolveAdaptiveNudgeGrowth2(modelContextLimit);
|
|
7255
8114
|
const growthFloor = Math.max(
|
|
7256
8115
|
config.compress?.minNudgeGrowthFloor ?? 5e3,
|
|
7257
8116
|
(config.compress?.minNudgeGrowthRatio ?? 0.45) * nudgeGrowthTokens
|
|
@@ -7266,7 +8125,7 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
7266
8125
|
const hasPendingNudge = state.nudges.lastNudgeShownTokens !== void 0;
|
|
7267
8126
|
const effectiveThreshold = hasPendingNudge ? Math.floor(nudgeGrowthTokens / 2) : nudgeGrowthTokens;
|
|
7268
8127
|
const growthReference = state.nudges.lastNudgeShownTokens ?? state.nudges.lastPerMessageNudgeTokens;
|
|
7269
|
-
const decision =
|
|
8128
|
+
const decision = computeShouldNudge2({
|
|
7270
8129
|
currentTokens,
|
|
7271
8130
|
modelContextLimit,
|
|
7272
8131
|
overMinLimit,
|
|
@@ -8000,7 +8859,7 @@ function buildSchema3() {
|
|
|
8000
8859
|
function extractMessageId(m) {
|
|
8001
8860
|
return m.id ?? m.messageId ?? "";
|
|
8002
8861
|
}
|
|
8003
|
-
function
|
|
8862
|
+
function extractMessageText2(m) {
|
|
8004
8863
|
const msg = m;
|
|
8005
8864
|
const role = msg.role || msg.type || "unknown";
|
|
8006
8865
|
const content = typeof msg.content === "string" ? msg.content : typeof msg.text === "string" ? msg.text : JSON.stringify(msg.content || msg.text || "");
|
|
@@ -8053,7 +8912,7 @@ function createDecompressTool(ctx) {
|
|
|
8053
8912
|
}
|
|
8054
8913
|
}
|
|
8055
8914
|
const blockMessages = rawMessages.filter((m) => msgIdSet.has(extractMessageId(m)));
|
|
8056
|
-
const lines2 = blockMessages.map(
|
|
8915
|
+
const lines2 = blockMessages.map(extractMessageText2);
|
|
8057
8916
|
const { writeFile: writeFile3 } = await import("fs/promises");
|
|
8058
8917
|
const fileContent = lines2.length > 0 ? lines2.join("\n\n---\n\n") : activeBlocks[0]?.summary ?? "(no content available)";
|
|
8059
8918
|
await writeFile3(targetPath, fileContent, "utf-8");
|
|
@@ -8774,7 +9633,7 @@ TOOLS
|
|
|
8774
9633
|
|
|
8775
9634
|
You have five context-management tools:
|
|
8776
9635
|
|
|
8777
|
-
- \`compress\` \u2014 Replace a contiguous range of older conversation with a single detailed summary you write. Use when content is genuinely consumed (no longer needed for the current task step).
|
|
9636
|
+
- \`compress\` \u2014 Replace a contiguous range of older conversation with a single detailed summary you write. Use when content is genuinely consumed (no longer needed for the current task step). Single range: \`compress({ topic: "API exploration", content: [{ startId: "m00150", endId: "m00220", summary: "..." }] })\`. Batch (multiple unrelated ranges, each with its own topic): \`compress({ content: [{ topic: "Auth", startId: "m00150", endId: "m00220", summary: "..." }, { topic: "Deploy", startId: "m00300", endId: "m00350", summary: "..." }] })\`.
|
|
8778
9637
|
- \`decompress\` \u2014 Restore a previously compressed block's full original content, optionally to a file for large blocks. Use when a summary lacks the exact detail you need. Example: \`decompress({ blockId: "b5" })\` or \`decompress({ blockId: "b5", toFile: "path" })\`.
|
|
8779
9638
|
- \`search_context\` \u2014 Search compressed block summaries (and optionally visible messages) by keyword. Use BEFORE decompressing to find the right block. Example: \`search_context({ query: "auth token refresh" })\`.
|
|
8780
9639
|
- \`prune\` \u2014 Remove old tool outputs by tool type, keeping only recent calls. Unlike compress (which creates summaries), prune directly strips outputs. Use for disposable outputs like old todowrite states or edit echoes. Example: \`prune({ toolType: "todowrite", keepLatest: 3 })\`.
|
|
@@ -8873,6 +9732,16 @@ Rules:
|
|
|
8873
9732
|
BATCHING
|
|
8874
9733
|
When multiple independent ranges are ready and their boundaries do not overlap, include all of them as separate entries in the \`content\` array of a single tool call. Each entry should have its own \`startId\`, \`endId\`, and \`summary\`.
|
|
8875
9734
|
|
|
9735
|
+
When the ranges cover unrelated topics, give each entry its own \`topic\` for better summary quality \u2014 do not force unrelated content under a single shared topic. Omit the top-level \`topic\` when every entry has its own. Use the top-level \`topic\` only as a fallback when entries don't specify one.
|
|
9736
|
+
|
|
9737
|
+
\`\`\`
|
|
9738
|
+
compress({ content: [
|
|
9739
|
+
{ topic: "Auth System Exploration", startId: "m00010", endId: "m00050", summary: "..." },
|
|
9740
|
+
{ topic: "Bug Hunt", startId: "m00060", endId: "m00080", summary: "..." },
|
|
9741
|
+
{ topic: "Deployment", startId: "m00090", endId: "m00110", summary: "..." },
|
|
9742
|
+
]})
|
|
9743
|
+
\`\`\`
|
|
9744
|
+
|
|
8876
9745
|
KEEP AND REF MARKERS
|
|
8877
9746
|
When writing a summary, you may embed markers that reference specific messages in the compressed range. The system resolves them automatically:
|
|
8878
9747
|
|
|
@@ -10567,46 +11436,9 @@ function createSystemPromptHandler(state, logger, config, prompts) {
|
|
|
10567
11436
|
};
|
|
10568
11437
|
}
|
|
10569
11438
|
function runMajorGC(state, config, logger, messages) {
|
|
10570
|
-
const maxBlockAge = config.gc.maxBlockAge ?? 15;
|
|
10571
|
-
let agedOutCount = 0;
|
|
10572
|
-
let agedOutTokens = 0;
|
|
10573
|
-
const now = Date.now();
|
|
10574
|
-
for (const [blockId, block] of state.prune.messages.blocksById) {
|
|
10575
|
-
if (!block.active) continue;
|
|
10576
|
-
const age = block.survivedCount ?? 0;
|
|
10577
|
-
if (age > maxBlockAge) {
|
|
10578
|
-
block.active = false;
|
|
10579
|
-
block.deactivatedAt = now;
|
|
10580
|
-
block.deactivatedByBlockId = void 0;
|
|
10581
|
-
state.prune.messages.activeBlockIds.delete(Number(blockId));
|
|
10582
|
-
const anchorMapped = state.prune.messages.activeByAnchorMessageId.get(block.anchorMessageId);
|
|
10583
|
-
if (anchorMapped === Number(blockId)) {
|
|
10584
|
-
state.prune.messages.activeByAnchorMessageId.delete(block.anchorMessageId);
|
|
10585
|
-
}
|
|
10586
|
-
agedOutCount++;
|
|
10587
|
-
agedOutTokens += block.summaryTokens ?? Math.round(block.summary.length / 4);
|
|
10588
|
-
}
|
|
10589
|
-
}
|
|
10590
|
-
if (agedOutCount > 0) {
|
|
10591
|
-
logger.info("Major GC: deactivated aged-out blocks", {
|
|
10592
|
-
agedOutCount,
|
|
10593
|
-
agedOutTokens,
|
|
10594
|
-
maxBlockAge
|
|
10595
|
-
});
|
|
10596
|
-
saveSessionState(state, logger).catch(() => {
|
|
10597
|
-
});
|
|
10598
|
-
}
|
|
10599
11439
|
if (!state.modelContextLimit) return;
|
|
10600
11440
|
const currentTokens = getCurrentTokenUsage(state, messages);
|
|
10601
|
-
|
|
10602
|
-
let hasOversizedBlocks = false;
|
|
10603
|
-
for (const [, block] of state.prune.messages.blocksById) {
|
|
10604
|
-
if (block.active && block.summary.length > oversizedThreshold) {
|
|
10605
|
-
hasOversizedBlocks = true;
|
|
10606
|
-
break;
|
|
10607
|
-
}
|
|
10608
|
-
}
|
|
10609
|
-
if (!shouldRunMajorGC(currentTokens, state.modelContextLimit, config.gc) && !hasOversizedBlocks) return;
|
|
11441
|
+
if (!shouldRunMajorGC(currentTokens, state.modelContextLimit, config.gc)) return;
|
|
10610
11442
|
const oldBlocks = [];
|
|
10611
11443
|
for (const [blockId, block] of state.prune.messages.blocksById) {
|
|
10612
11444
|
if (!block.active) continue;
|