opencode-acp 1.6.1 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -0
- package/README.zh-CN.md +28 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +283 -222
- package/dist/index.js.map +1 -1
- package/dist/lib/compress/decompress.d.ts.map +1 -1
- package/dist/lib/compress/index.d.ts +1 -0
- package/dist/lib/compress/index.d.ts.map +1 -1
- package/dist/lib/compress/message.d.ts.map +1 -1
- package/dist/lib/compress/range.d.ts.map +1 -1
- package/dist/lib/compress/search.d.ts +3 -1
- package/dist/lib/compress/search.d.ts.map +1 -1
- package/dist/lib/config-validation.d.ts.map +1 -1
- package/dist/lib/config.d.ts +1 -2
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/messages/inject/inject.d.ts.map +1 -1
- package/dist/lib/messages/inject/utils.d.ts +1 -1
- package/dist/lib/messages/inject/utils.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/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -894,13 +894,12 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
|
894
894
|
"compress.modelMaxLimits",
|
|
895
895
|
"compress.modelMinLimits",
|
|
896
896
|
"compress.nudgeFrequency",
|
|
897
|
-
"compress.
|
|
897
|
+
"compress.minNudgeContextPercent",
|
|
898
898
|
"compress.iterationNudgeThreshold",
|
|
899
899
|
"compress.nudgeForce",
|
|
900
900
|
"compress.protectedTools",
|
|
901
901
|
"compress.protectTags",
|
|
902
902
|
"compress.protectUserMessages",
|
|
903
|
-
"compress.maxSummaryLength",
|
|
904
903
|
"compress.maxSummaryLengthHard",
|
|
905
904
|
"compress.minCompressRange",
|
|
906
905
|
"gc",
|
|
@@ -1121,13 +1120,6 @@ function validateConfigTypes(config) {
|
|
|
1121
1120
|
actual: `${compress.nudgeFrequency} (will be clamped to 1)`
|
|
1122
1121
|
});
|
|
1123
1122
|
}
|
|
1124
|
-
if (compress.perMessageNudgeGrowthPercent !== void 0 && typeof compress.perMessageNudgeGrowthPercent !== "number") {
|
|
1125
|
-
errors.push({
|
|
1126
|
-
key: "compress.perMessageNudgeGrowthPercent",
|
|
1127
|
-
expected: "number",
|
|
1128
|
-
actual: typeof compress.perMessageNudgeGrowthPercent
|
|
1129
|
-
});
|
|
1130
|
-
}
|
|
1131
1123
|
if (compress.iterationNudgeThreshold !== void 0 && typeof compress.iterationNudgeThreshold !== "number") {
|
|
1132
1124
|
errors.push({
|
|
1133
1125
|
key: "compress.iterationNudgeThreshold",
|
|
@@ -1163,20 +1155,6 @@ function validateConfigTypes(config) {
|
|
|
1163
1155
|
actual: typeof compress.protectUserMessages
|
|
1164
1156
|
});
|
|
1165
1157
|
}
|
|
1166
|
-
if (compress.maxSummaryLength !== void 0 && typeof compress.maxSummaryLength !== "number") {
|
|
1167
|
-
errors.push({
|
|
1168
|
-
key: "compress.maxSummaryLength",
|
|
1169
|
-
expected: "number",
|
|
1170
|
-
actual: typeof compress.maxSummaryLength
|
|
1171
|
-
});
|
|
1172
|
-
}
|
|
1173
|
-
if (typeof compress.maxSummaryLength === "number" && compress.maxSummaryLength < 1) {
|
|
1174
|
-
errors.push({
|
|
1175
|
-
key: "compress.maxSummaryLength",
|
|
1176
|
-
expected: "positive number (>= 1)",
|
|
1177
|
-
actual: `${compress.maxSummaryLength}`
|
|
1178
|
-
});
|
|
1179
|
-
}
|
|
1180
1158
|
if (compress.maxSummaryLengthHard !== void 0 && typeof compress.maxSummaryLengthHard !== "number") {
|
|
1181
1159
|
errors.push({
|
|
1182
1160
|
key: "compress.maxSummaryLengthHard",
|
|
@@ -1191,13 +1169,6 @@ function validateConfigTypes(config) {
|
|
|
1191
1169
|
actual: `${compress.maxSummaryLengthHard}`
|
|
1192
1170
|
});
|
|
1193
1171
|
}
|
|
1194
|
-
if (typeof compress.maxSummaryLength === "number" && typeof compress.maxSummaryLengthHard === "number" && compress.maxSummaryLengthHard < compress.maxSummaryLength) {
|
|
1195
|
-
errors.push({
|
|
1196
|
-
key: "compress.maxSummaryLengthHard",
|
|
1197
|
-
expected: `>= maxSummaryLength (${compress.maxSummaryLength})`,
|
|
1198
|
-
actual: `${compress.maxSummaryLengthHard}`
|
|
1199
|
-
});
|
|
1200
|
-
}
|
|
1201
1172
|
if (compress.minCompressRange !== void 0 && typeof compress.minCompressRange !== "number") {
|
|
1202
1173
|
errors.push({
|
|
1203
1174
|
key: "compress.minCompressRange",
|
|
@@ -1519,14 +1490,13 @@ var defaultConfig = {
|
|
|
1519
1490
|
maxContextLimit: "55%",
|
|
1520
1491
|
minContextLimit: "45%",
|
|
1521
1492
|
nudgeFrequency: 5,
|
|
1522
|
-
|
|
1493
|
+
minNudgeContextPercent: 15,
|
|
1523
1494
|
iterationNudgeThreshold: 15,
|
|
1524
1495
|
nudgeForce: "soft",
|
|
1525
1496
|
protectedTools: [...COMPRESS_DEFAULT_PROTECTED_TOOLS],
|
|
1526
1497
|
protectTags: false,
|
|
1527
1498
|
protectUserMessages: false,
|
|
1528
|
-
|
|
1529
|
-
maxSummaryLengthHard: 800,
|
|
1499
|
+
maxSummaryLengthHard: 4e3,
|
|
1530
1500
|
minCompressRange: 2e3
|
|
1531
1501
|
},
|
|
1532
1502
|
strategies: {
|
|
@@ -1674,13 +1644,12 @@ function mergeCompress(base, override) {
|
|
|
1674
1644
|
modelMaxLimits: override.modelMaxLimits ?? base.modelMaxLimits,
|
|
1675
1645
|
modelMinLimits: override.modelMinLimits ?? base.modelMinLimits,
|
|
1676
1646
|
nudgeFrequency: override.nudgeFrequency ?? base.nudgeFrequency,
|
|
1677
|
-
|
|
1647
|
+
minNudgeContextPercent: override.minNudgeContextPercent ?? base.minNudgeContextPercent,
|
|
1678
1648
|
iterationNudgeThreshold: override.iterationNudgeThreshold ?? base.iterationNudgeThreshold,
|
|
1679
1649
|
nudgeForce: override.nudgeForce ?? base.nudgeForce,
|
|
1680
1650
|
protectedTools: [.../* @__PURE__ */ new Set([...base.protectedTools, ...override.protectedTools ?? []])],
|
|
1681
1651
|
protectTags: override.protectTags ?? base.protectTags,
|
|
1682
1652
|
protectUserMessages: override.protectUserMessages ?? base.protectUserMessages,
|
|
1683
|
-
maxSummaryLength: override.maxSummaryLength ?? base.maxSummaryLength,
|
|
1684
1653
|
maxSummaryLengthHard: override.maxSummaryLengthHard ?? base.maxSummaryLengthHard,
|
|
1685
1654
|
minCompressRange: override.minCompressRange ?? base.minCompressRange
|
|
1686
1655
|
};
|
|
@@ -1841,7 +1810,7 @@ Using previous/default values`
|
|
|
1841
1810
|
}
|
|
1842
1811
|
|
|
1843
1812
|
// lib/compress/message.ts
|
|
1844
|
-
import { tool } from "@opencode-ai/plugin";
|
|
1813
|
+
import { tool as tool2 } from "@opencode-ai/plugin";
|
|
1845
1814
|
|
|
1846
1815
|
// lib/token-utils.ts
|
|
1847
1816
|
import * as _anthropicTokenizer from "@anthropic-ai/tokenizer";
|
|
@@ -2219,6 +2188,7 @@ function allocateNextMessageRef(state) {
|
|
|
2219
2188
|
}
|
|
2220
2189
|
|
|
2221
2190
|
// lib/compress/search.ts
|
|
2191
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2222
2192
|
async function fetchSessionMessages(client, sessionId) {
|
|
2223
2193
|
const response = await client.session.messages({
|
|
2224
2194
|
path: { id: sessionId }
|
|
@@ -2430,6 +2400,120 @@ function buildBoundaryLookup(context, state) {
|
|
|
2430
2400
|
}
|
|
2431
2401
|
return lookup;
|
|
2432
2402
|
}
|
|
2403
|
+
var SEARCH_CONTEXT_TOOL_DESCRIPTION = `Search through all compressed block summaries AND visible messages to find relevant content. Use this BEFORE decompressing to find the right block. Returns a hit list with block/message IDs, relevance scores, and previews.
|
|
2404
|
+
|
|
2405
|
+
Examples:
|
|
2406
|
+
- search_context({ query: "decoder accuracy" }) \u2014 find blocks/messages about decoder accuracy
|
|
2407
|
+
- search_context({ query: "training loss PPL" }) \u2014 find training results
|
|
2408
|
+
- search_context({ query: "architecture design", limit: 5 }) \u2014 top 5 results`;
|
|
2409
|
+
function countOccurrences(text, term) {
|
|
2410
|
+
if (!text || !term) return 0;
|
|
2411
|
+
let count = 0;
|
|
2412
|
+
let idx = 0;
|
|
2413
|
+
while ((idx = text.indexOf(term, idx)) !== -1) {
|
|
2414
|
+
count++;
|
|
2415
|
+
idx += term.length;
|
|
2416
|
+
}
|
|
2417
|
+
return count;
|
|
2418
|
+
}
|
|
2419
|
+
function buildSearchPreview(text, firstTerm) {
|
|
2420
|
+
if (!text) return "";
|
|
2421
|
+
const matchIdx = text.toLowerCase().indexOf(firstTerm);
|
|
2422
|
+
if (matchIdx >= 0) {
|
|
2423
|
+
const start = Math.max(0, matchIdx - 50);
|
|
2424
|
+
const end = Math.min(text.length, matchIdx + 150);
|
|
2425
|
+
return (start > 0 ? "..." : "") + text.substring(start, end) + (end < text.length ? "..." : "");
|
|
2426
|
+
}
|
|
2427
|
+
return text.substring(0, 200) + (text.length > 200 ? "..." : "");
|
|
2428
|
+
}
|
|
2429
|
+
function createSearchContextTool(ctx) {
|
|
2430
|
+
ctx.prompts.reload();
|
|
2431
|
+
return tool({
|
|
2432
|
+
description: SEARCH_CONTEXT_TOOL_DESCRIPTION,
|
|
2433
|
+
args: {
|
|
2434
|
+
query: tool.schema.string().describe("Search query \u2014 keywords or phrase to find"),
|
|
2435
|
+
limit: tool.schema.number().optional().describe("Maximum results to return (default: 10)"),
|
|
2436
|
+
deep: tool.schema.boolean().optional().describe("If true, also search visible (uncompressed) messages. Slower but more thorough (default: false)")
|
|
2437
|
+
},
|
|
2438
|
+
async execute(args) {
|
|
2439
|
+
const query = (args.query || "").toLowerCase().trim();
|
|
2440
|
+
const limit = args.limit ?? 10;
|
|
2441
|
+
if (!query) {
|
|
2442
|
+
return "Error: query is required.";
|
|
2443
|
+
}
|
|
2444
|
+
const queryTerms = query.split(/\s+/).filter((t) => t.length > 0);
|
|
2445
|
+
const results = [];
|
|
2446
|
+
const MIN_RELEVANCE = 0.1;
|
|
2447
|
+
const blocksById = ctx.state.prune.messages.blocksById;
|
|
2448
|
+
for (const [blockId, block] of blocksById) {
|
|
2449
|
+
if (!block.active) continue;
|
|
2450
|
+
const topic = (block.topic || "").toLowerCase();
|
|
2451
|
+
const summary = (block.summary || "").toLowerCase();
|
|
2452
|
+
let relevance = 0;
|
|
2453
|
+
let termsHit = 0;
|
|
2454
|
+
for (const term of queryTerms) {
|
|
2455
|
+
let termHit = false;
|
|
2456
|
+
const topicCount = countOccurrences(topic, term);
|
|
2457
|
+
if (topicCount > 0) {
|
|
2458
|
+
relevance += Math.min(topicCount * 0.15, 0.45);
|
|
2459
|
+
termHit = true;
|
|
2460
|
+
}
|
|
2461
|
+
const summaryCount = countOccurrences(summary, term);
|
|
2462
|
+
if (summaryCount > 0) {
|
|
2463
|
+
relevance += Math.min(summaryCount * 0.04, 0.2);
|
|
2464
|
+
termHit = true;
|
|
2465
|
+
}
|
|
2466
|
+
if (termHit) termsHit++;
|
|
2467
|
+
}
|
|
2468
|
+
if (termsHit === queryTerms.length && queryTerms.length > 1) {
|
|
2469
|
+
relevance *= 1.2;
|
|
2470
|
+
}
|
|
2471
|
+
if (queryTerms.length > 1 && query.includes(" ")) {
|
|
2472
|
+
if (topic.includes(query) || summary.includes(query)) {
|
|
2473
|
+
relevance += 0.25;
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
relevance = Math.min(relevance, 1);
|
|
2477
|
+
if (relevance < MIN_RELEVANCE) continue;
|
|
2478
|
+
const origSummary = block.summary || "";
|
|
2479
|
+
const preview = buildSearchPreview(origSummary, queryTerms[0]);
|
|
2480
|
+
results.push({
|
|
2481
|
+
type: "block",
|
|
2482
|
+
id: `b${blockId}`,
|
|
2483
|
+
relevance,
|
|
2484
|
+
label: block.topic || "(no topic)",
|
|
2485
|
+
preview,
|
|
2486
|
+
action: `\u2192 decompress(b${blockId}) for full content`
|
|
2487
|
+
});
|
|
2488
|
+
}
|
|
2489
|
+
results.sort((a, b) => b.relevance - a.relevance);
|
|
2490
|
+
const limited = results.slice(0, limit);
|
|
2491
|
+
if (limited.length === 0) {
|
|
2492
|
+
return `No matches found for "${args.query}". Try different keywords.`;
|
|
2493
|
+
}
|
|
2494
|
+
const lines = [];
|
|
2495
|
+
lines.push(
|
|
2496
|
+
`\u{1F50D} Found ${results.length} matches for "${args.query}" (showing top ${limited.length}):`
|
|
2497
|
+
);
|
|
2498
|
+
lines.push("");
|
|
2499
|
+
for (const result of limited) {
|
|
2500
|
+
const icon = result.type === "block" ? "\u{1F4E6}" : "\u{1F4C4}";
|
|
2501
|
+
const stars = "\u2B50".repeat(Math.ceil(result.relevance * 5));
|
|
2502
|
+
lines.push(
|
|
2503
|
+
`${icon} [${result.id}] ${stars} (${result.relevance.toFixed(2)}) "${result.label}"`
|
|
2504
|
+
);
|
|
2505
|
+
lines.push(` ${result.preview}`);
|
|
2506
|
+
lines.push(` ${result.action}`);
|
|
2507
|
+
lines.push("");
|
|
2508
|
+
}
|
|
2509
|
+
let output = lines.join("\n");
|
|
2510
|
+
if (output.length > 3e3) {
|
|
2511
|
+
output = output.substring(0, 3e3) + "\n... (truncated, refine query for more specific results)";
|
|
2512
|
+
}
|
|
2513
|
+
return output;
|
|
2514
|
+
}
|
|
2515
|
+
});
|
|
2516
|
+
}
|
|
2433
2517
|
|
|
2434
2518
|
// lib/compress/state.ts
|
|
2435
2519
|
var DEFAULT_PROMOTION_THRESHOLD = 5;
|
|
@@ -3620,20 +3704,20 @@ function matchesGlob(inputPath, pattern) {
|
|
|
3620
3704
|
regex += "$";
|
|
3621
3705
|
return new RegExp(regex).test(input);
|
|
3622
3706
|
}
|
|
3623
|
-
function getFilePathsFromParameters(
|
|
3707
|
+
function getFilePathsFromParameters(tool5, parameters) {
|
|
3624
3708
|
if (typeof parameters !== "object" || parameters === null) {
|
|
3625
3709
|
return [];
|
|
3626
3710
|
}
|
|
3627
3711
|
const paths = [];
|
|
3628
3712
|
const params = parameters;
|
|
3629
|
-
if (
|
|
3713
|
+
if (tool5 === "apply_patch" && typeof params.patchText === "string") {
|
|
3630
3714
|
const pathRegex = /\*\*\* (?:Add|Delete|Update) File: ([^\n\r]+)/g;
|
|
3631
3715
|
let match;
|
|
3632
3716
|
while ((match = pathRegex.exec(params.patchText)) !== null) {
|
|
3633
3717
|
paths.push(match[1].trim());
|
|
3634
3718
|
}
|
|
3635
3719
|
}
|
|
3636
|
-
if (
|
|
3720
|
+
if (tool5 === "multiedit") {
|
|
3637
3721
|
if (typeof params.filePath === "string") {
|
|
3638
3722
|
paths.push(params.filePath);
|
|
3639
3723
|
}
|
|
@@ -3728,13 +3812,13 @@ var deduplicate = (state, logger, config, messages) => {
|
|
|
3728
3812
|
logger.debug(`Marked ${newPruneIds.length} duplicate tool calls for pruning`);
|
|
3729
3813
|
}
|
|
3730
3814
|
};
|
|
3731
|
-
function createToolSignature(
|
|
3815
|
+
function createToolSignature(tool5, parameters) {
|
|
3732
3816
|
if (!parameters) {
|
|
3733
|
-
return
|
|
3817
|
+
return tool5;
|
|
3734
3818
|
}
|
|
3735
3819
|
const normalized = normalizeParameters(parameters);
|
|
3736
3820
|
const sorted = sortObjectKeys(normalized);
|
|
3737
|
-
return `${
|
|
3821
|
+
return `${tool5}::${JSON.stringify(sorted)}`;
|
|
3738
3822
|
}
|
|
3739
3823
|
function normalizeParameters(params) {
|
|
3740
3824
|
if (typeof params !== "object" || params === null) return params;
|
|
@@ -3809,9 +3893,9 @@ var purgeErrors = (state, logger, config, messages) => {
|
|
|
3809
3893
|
};
|
|
3810
3894
|
|
|
3811
3895
|
// lib/ui/utils.ts
|
|
3812
|
-
function extractParameterKey(
|
|
3896
|
+
function extractParameterKey(tool5, parameters) {
|
|
3813
3897
|
if (!parameters) return "";
|
|
3814
|
-
if (
|
|
3898
|
+
if (tool5 === "read" && parameters.filePath) {
|
|
3815
3899
|
const offset = parameters.offset;
|
|
3816
3900
|
const limit = parameters.limit;
|
|
3817
3901
|
if (offset !== void 0 && limit !== void 0) {
|
|
@@ -3825,10 +3909,10 @@ function extractParameterKey(tool4, parameters) {
|
|
|
3825
3909
|
}
|
|
3826
3910
|
return parameters.filePath;
|
|
3827
3911
|
}
|
|
3828
|
-
if ((
|
|
3912
|
+
if ((tool5 === "write" || tool5 === "edit" || tool5 === "multiedit") && parameters.filePath) {
|
|
3829
3913
|
return parameters.filePath;
|
|
3830
3914
|
}
|
|
3831
|
-
if (
|
|
3915
|
+
if (tool5 === "apply_patch" && typeof parameters.patchText === "string") {
|
|
3832
3916
|
const pathRegex = /\*\*\* (?:Add|Delete|Update) File: ([^\n\r]+)/g;
|
|
3833
3917
|
const paths = [];
|
|
3834
3918
|
let match;
|
|
@@ -3845,51 +3929,51 @@ function extractParameterKey(tool4, parameters) {
|
|
|
3845
3929
|
}
|
|
3846
3930
|
return "patch";
|
|
3847
3931
|
}
|
|
3848
|
-
if (
|
|
3932
|
+
if (tool5 === "list") {
|
|
3849
3933
|
return parameters.path || "(current directory)";
|
|
3850
3934
|
}
|
|
3851
|
-
if (
|
|
3935
|
+
if (tool5 === "glob") {
|
|
3852
3936
|
if (parameters.pattern) {
|
|
3853
3937
|
const pathInfo = parameters.path ? ` in ${parameters.path}` : "";
|
|
3854
3938
|
return `"${parameters.pattern}"${pathInfo}`;
|
|
3855
3939
|
}
|
|
3856
3940
|
return "(unknown pattern)";
|
|
3857
3941
|
}
|
|
3858
|
-
if (
|
|
3942
|
+
if (tool5 === "grep") {
|
|
3859
3943
|
if (parameters.pattern) {
|
|
3860
3944
|
const pathInfo = parameters.path ? ` in ${parameters.path}` : "";
|
|
3861
3945
|
return `"${parameters.pattern}"${pathInfo}`;
|
|
3862
3946
|
}
|
|
3863
3947
|
return "(unknown pattern)";
|
|
3864
3948
|
}
|
|
3865
|
-
if (
|
|
3949
|
+
if (tool5 === "bash") {
|
|
3866
3950
|
if (parameters.description) return parameters.description;
|
|
3867
3951
|
if (parameters.command) {
|
|
3868
3952
|
return parameters.command.length > 50 ? parameters.command.substring(0, 50) + "..." : parameters.command;
|
|
3869
3953
|
}
|
|
3870
3954
|
}
|
|
3871
|
-
if (
|
|
3955
|
+
if (tool5 === "webfetch" && parameters.url) {
|
|
3872
3956
|
return parameters.url;
|
|
3873
3957
|
}
|
|
3874
|
-
if (
|
|
3958
|
+
if (tool5 === "websearch" && parameters.query) {
|
|
3875
3959
|
return `"${parameters.query}"`;
|
|
3876
3960
|
}
|
|
3877
|
-
if (
|
|
3961
|
+
if (tool5 === "codesearch" && parameters.query) {
|
|
3878
3962
|
return `"${parameters.query}"`;
|
|
3879
3963
|
}
|
|
3880
|
-
if (
|
|
3964
|
+
if (tool5 === "todowrite") {
|
|
3881
3965
|
return `${parameters.todos?.length || 0} todos`;
|
|
3882
3966
|
}
|
|
3883
|
-
if (
|
|
3967
|
+
if (tool5 === "todoread") {
|
|
3884
3968
|
return "read todo list";
|
|
3885
3969
|
}
|
|
3886
|
-
if (
|
|
3970
|
+
if (tool5 === "task" && parameters.description) {
|
|
3887
3971
|
return parameters.description;
|
|
3888
3972
|
}
|
|
3889
|
-
if (
|
|
3973
|
+
if (tool5 === "skill" && parameters.name) {
|
|
3890
3974
|
return parameters.name;
|
|
3891
3975
|
}
|
|
3892
|
-
if (
|
|
3976
|
+
if (tool5 === "lsp") {
|
|
3893
3977
|
const op = parameters.operation || "lsp";
|
|
3894
3978
|
const path = parameters.filePath || "";
|
|
3895
3979
|
const line = parameters.line;
|
|
@@ -3902,7 +3986,7 @@ function extractParameterKey(tool4, parameters) {
|
|
|
3902
3986
|
}
|
|
3903
3987
|
return op;
|
|
3904
3988
|
}
|
|
3905
|
-
if (
|
|
3989
|
+
if (tool5 === "question") {
|
|
3906
3990
|
const questions = parameters.questions;
|
|
3907
3991
|
if (Array.isArray(questions) && questions.length > 0) {
|
|
3908
3992
|
const headers = questions.map((q) => q.header || "").filter(Boolean).slice(0, 3);
|
|
@@ -4481,36 +4565,40 @@ ${output}`);
|
|
|
4481
4565
|
}
|
|
4482
4566
|
|
|
4483
4567
|
// lib/compress/message.ts
|
|
4484
|
-
function buildSchema(
|
|
4568
|
+
function buildSchema() {
|
|
4485
4569
|
return {
|
|
4486
|
-
topic:
|
|
4570
|
+
topic: tool2.schema.string().describe(
|
|
4487
4571
|
"Short label (3-5 words) for the overall batch - e.g., 'Closed Research Notes'"
|
|
4488
4572
|
),
|
|
4489
|
-
content:
|
|
4490
|
-
|
|
4491
|
-
messageId:
|
|
4492
|
-
topic:
|
|
4493
|
-
summary:
|
|
4494
|
-
|
|
4573
|
+
content: tool2.schema.array(
|
|
4574
|
+
tool2.schema.object({
|
|
4575
|
+
messageId: tool2.schema.string().describe("Raw message ID to compress (e.g. m00001)"),
|
|
4576
|
+
topic: tool2.schema.string().describe("Short label (3-5 words) for this one message summary"),
|
|
4577
|
+
summary: tool2.schema.string().describe(
|
|
4578
|
+
"Complete technical summary replacing that one message. Keep only essential details (conclusions, file paths, decisions, exact values, etc.)."
|
|
4495
4579
|
)
|
|
4496
4580
|
})
|
|
4497
|
-
).describe("Batch of individual message summaries to create in one tool call")
|
|
4581
|
+
).describe("Batch of individual message summaries to create in one tool call"),
|
|
4582
|
+
summaryMaxChars: tool2.schema.number().optional().describe("Override max summary length (default max: 4000 chars). Use when content is important and needs more detail \u2014 don't lose critical info just to fit the limit.")
|
|
4498
4583
|
};
|
|
4499
4584
|
}
|
|
4500
4585
|
function createCompressMessageTool(ctx) {
|
|
4501
4586
|
ctx.prompts.reload();
|
|
4502
4587
|
const runtimePrompts = ctx.prompts.getRuntimePrompts();
|
|
4503
|
-
return
|
|
4588
|
+
return tool2({
|
|
4504
4589
|
description: runtimePrompts.compressMessage + MESSAGE_FORMAT_EXTENSION,
|
|
4505
|
-
args: buildSchema(
|
|
4590
|
+
args: buildSchema(),
|
|
4506
4591
|
async execute(args, toolCtx) {
|
|
4507
4592
|
const input = args;
|
|
4508
4593
|
validateArgs(input);
|
|
4509
|
-
const
|
|
4594
|
+
const maxLen = args.summaryMaxChars ?? ctx.config.compress.maxSummaryLengthHard;
|
|
4510
4595
|
for (const entry of input.content) {
|
|
4511
|
-
if (entry.summary.length >
|
|
4596
|
+
if (entry.summary.length > maxLen) {
|
|
4512
4597
|
throw new Error(
|
|
4513
|
-
`Summary too long (${entry.summary.length} chars,
|
|
4598
|
+
`Summary too long (${entry.summary.length} chars, max ${maxLen}).
|
|
4599
|
+
1. If this summary is nearly the same size as the original content, it may not be worth compressing \u2014 skip it.
|
|
4600
|
+
2. Strip noise (failed attempts, verbose outputs) but keep project-critical details (file paths, decisions, exact values).
|
|
4601
|
+
3. For important content needing detail, pass summaryMaxChars to increase the limit \u2014 don't lose critical info just to fit. Example: add "summaryMaxChars": 6000 to the tool call args.`
|
|
4514
4602
|
);
|
|
4515
4603
|
}
|
|
4516
4604
|
}
|
|
@@ -4613,7 +4701,7 @@ function createCompressMessageTool(ctx) {
|
|
|
4613
4701
|
}
|
|
4614
4702
|
|
|
4615
4703
|
// lib/compress/range.ts
|
|
4616
|
-
import { tool as
|
|
4704
|
+
import { tool as tool3 } from "@opencode-ai/plugin";
|
|
4617
4705
|
|
|
4618
4706
|
// lib/compress/range-utils.ts
|
|
4619
4707
|
var BLOCK_PLACEHOLDER_REGEX = /\(b(\d+)\)|\{block_(\d+)\}/gi;
|
|
@@ -4755,38 +4843,42 @@ function appendMissingBlockSummaries(summary, _missingBlockIds, _summaryByBlockI
|
|
|
4755
4843
|
}
|
|
4756
4844
|
|
|
4757
4845
|
// lib/compress/range.ts
|
|
4758
|
-
function buildSchema2(
|
|
4846
|
+
function buildSchema2() {
|
|
4759
4847
|
return {
|
|
4760
|
-
topic:
|
|
4761
|
-
content:
|
|
4762
|
-
|
|
4763
|
-
startId:
|
|
4848
|
+
topic: tool3.schema.string().describe("Short label (3-5 words) for display - e.g., 'Auth System Exploration'"),
|
|
4849
|
+
content: tool3.schema.array(
|
|
4850
|
+
tool3.schema.object({
|
|
4851
|
+
startId: tool3.schema.string().describe(
|
|
4764
4852
|
"Message or block ID marking the beginning of range (e.g. m00001, b2)"
|
|
4765
4853
|
),
|
|
4766
|
-
endId:
|
|
4767
|
-
summary:
|
|
4768
|
-
|
|
4854
|
+
endId: tool3.schema.string().describe("Message or block ID marking the end of range (e.g. m00012, b5)"),
|
|
4855
|
+
summary: tool3.schema.string().describe(
|
|
4856
|
+
"Complete technical summary replacing all content in range. Keep only essential details (conclusions, file paths, decisions, exact values, etc.)."
|
|
4769
4857
|
)
|
|
4770
4858
|
})
|
|
4771
4859
|
).describe(
|
|
4772
4860
|
"One or more ranges to compress, each with start/end boundaries and a summary"
|
|
4773
|
-
)
|
|
4861
|
+
),
|
|
4862
|
+
summaryMaxChars: tool3.schema.number().optional().describe("Override max summary length (default max: 4000 chars). Use when content is important and needs more detail \u2014 don't lose critical info just to fit the limit.")
|
|
4774
4863
|
};
|
|
4775
4864
|
}
|
|
4776
4865
|
function createCompressRangeTool(ctx) {
|
|
4777
4866
|
ctx.prompts.reload();
|
|
4778
4867
|
const runtimePrompts = ctx.prompts.getRuntimePrompts();
|
|
4779
|
-
return
|
|
4868
|
+
return tool3({
|
|
4780
4869
|
description: runtimePrompts.compressRange + RANGE_FORMAT_EXTENSION,
|
|
4781
|
-
args: buildSchema2(
|
|
4870
|
+
args: buildSchema2(),
|
|
4782
4871
|
async execute(args, toolCtx) {
|
|
4783
4872
|
const input = args;
|
|
4784
4873
|
validateArgs2(input);
|
|
4785
|
-
const
|
|
4874
|
+
const maxLen = args.summaryMaxChars ?? ctx.config.compress.maxSummaryLengthHard;
|
|
4786
4875
|
for (const entry of input.content) {
|
|
4787
|
-
if (entry.summary.length >
|
|
4876
|
+
if (entry.summary.length > maxLen) {
|
|
4788
4877
|
throw new Error(
|
|
4789
|
-
`Summary too long (${entry.summary.length} chars,
|
|
4878
|
+
`Summary too long (${entry.summary.length} chars, max ${maxLen}).
|
|
4879
|
+
1. If this summary is nearly the same size as the original content, it may not be worth compressing \u2014 skip it.
|
|
4880
|
+
2. Strip noise (failed attempts, verbose outputs) but keep project-critical details (file paths, decisions, exact values).
|
|
4881
|
+
3. For important content needing detail, pass summaryMaxChars to increase the limit \u2014 don't lose critical info just to fit. Example: add "summaryMaxChars": 6000 to the tool call args.`
|
|
4790
4882
|
);
|
|
4791
4883
|
}
|
|
4792
4884
|
}
|
|
@@ -4923,7 +5015,8 @@ function createCompressRangeTool(ctx) {
|
|
|
4923
5015
|
}
|
|
4924
5016
|
await finalizeSession(ctx, toolCtx, rawMessages, notifications, input.topic);
|
|
4925
5017
|
return `Compressed ${totalCompressedMessages} messages into ${COMPRESSED_BLOCK_HEADER}.
|
|
4926
|
-
IMPORTANT: This was an automatic context compression. You MUST continue your previous task exactly where you left off. Do NOT ask the user what to do next
|
|
5018
|
+
IMPORTANT: This was an automatic context compression. You MUST continue your previous task exactly where you left off. Do NOT ask the user what to do next.
|
|
5019
|
+
\u{1F4A1} Tip: Use search_context('keyword') to find compressed content when you need it later.`;
|
|
4927
5020
|
}
|
|
4928
5021
|
});
|
|
4929
5022
|
}
|
|
@@ -4940,7 +5033,7 @@ function extractBoundaryConsumedBlocks(startReference, endReference) {
|
|
|
4940
5033
|
}
|
|
4941
5034
|
|
|
4942
5035
|
// lib/compress/decompress.ts
|
|
4943
|
-
import { tool as
|
|
5036
|
+
import { tool as tool4 } from "@opencode-ai/plugin";
|
|
4944
5037
|
|
|
4945
5038
|
// lib/messages/utils.ts
|
|
4946
5039
|
import { createHash } from "crypto";
|
|
@@ -4951,10 +5044,10 @@ var MERGED_SUMMARY_FOOTER = `
|
|
|
4951
5044
|
[End ACP compressed context summary]
|
|
4952
5045
|
|
|
4953
5046
|
`;
|
|
4954
|
-
var DCP_BLOCK_ID_TAG_REGEX = /(<dcp-message-id(?=[\s>])[^>]*>)b\d+(<\/dcp-message-id>)/g;
|
|
4955
|
-
var DCP_MESSAGE_REF_TAG_REGEX = /<dcp-message-id>m\d+<\/dcp-message-id>/g;
|
|
4956
|
-
var DCP_PAIRED_TAG_REGEX = /<dcp[^>]*>[\s\S]*?<\/dcp[^>]*>/gi;
|
|
4957
|
-
var DCP_UNPAIRED_TAG_REGEX = /<\/?dcp[^>]*>/gi;
|
|
5047
|
+
var DCP_BLOCK_ID_TAG_REGEX = /(<dcp-message-id(?=[\s>])[^>]*>)b\d+(<\/(?:dcp|acp)-message-id>)/g;
|
|
5048
|
+
var DCP_MESSAGE_REF_TAG_REGEX = /<dcp-message-id>m\d+<\/(?:dcp|acp)-message-id>/g;
|
|
5049
|
+
var DCP_PAIRED_TAG_REGEX = /<dcp[^>]*>[\s\S]*?<\/(?:dcp|acp)[^>]*>/gi;
|
|
5050
|
+
var DCP_UNPAIRED_TAG_REGEX = /<\/?(?:dcp|acp)[^>]*>/gi;
|
|
4958
5051
|
var generateStableId = (prefix, seed) => {
|
|
4959
5052
|
const hash = createHash("sha256").update(seed).digest("hex").slice(0, SUMMARY_ID_HASH_LENGTH);
|
|
4960
5053
|
return `${prefix}_${hash}`;
|
|
@@ -5392,8 +5485,8 @@ var resolveEffectiveCompressPermission = (basePermission, hostPermissions, agent
|
|
|
5392
5485
|
agentName ? hostPermissions.agents[agentName] : void 0
|
|
5393
5486
|
) ? "deny" : basePermission;
|
|
5394
5487
|
};
|
|
5395
|
-
var hasExplicitToolPermission = (permissionConfig,
|
|
5396
|
-
return permissionConfig ? Object.prototype.hasOwnProperty.call(permissionConfig,
|
|
5488
|
+
var hasExplicitToolPermission = (permissionConfig, tool5) => {
|
|
5489
|
+
return permissionConfig ? Object.prototype.hasOwnProperty.call(permissionConfig, tool5) : false;
|
|
5397
5490
|
};
|
|
5398
5491
|
|
|
5399
5492
|
// lib/compress-permission.ts
|
|
@@ -5412,7 +5505,11 @@ var syncCompressPermissionState = (state, config, hostPermissions, messages) =>
|
|
|
5412
5505
|
// lib/prompts/extensions/nudge.ts
|
|
5413
5506
|
function buildCompressedBlockGuidance(state, gcConfig, context) {
|
|
5414
5507
|
const activeBlockIds = Array.from(state.prune.messages.activeBlockIds).filter((id) => Number.isInteger(id) && id > 0).sort((a, b) => a - b);
|
|
5415
|
-
const refs = activeBlockIds.map((id) =>
|
|
5508
|
+
const refs = activeBlockIds.map((id) => {
|
|
5509
|
+
const block = state.prune.messages.blocksById.get(id);
|
|
5510
|
+
const tokens = block?.summaryTokens ?? 0;
|
|
5511
|
+
return `b${id}${tokens > 0 ? ` (${tokens}t)` : ""}`;
|
|
5512
|
+
});
|
|
5416
5513
|
const blockCount = refs.length;
|
|
5417
5514
|
let blockList;
|
|
5418
5515
|
if (blockCount <= 20) {
|
|
@@ -5423,12 +5520,10 @@ function buildCompressedBlockGuidance(state, gcConfig, context) {
|
|
|
5423
5520
|
}
|
|
5424
5521
|
const includeHint = context?.includeHint ?? true;
|
|
5425
5522
|
const lines = [
|
|
5426
|
-
|
|
5427
|
-
`- Active compressed blocks: ${blockCount} (${blockList})`,
|
|
5428
|
-
"- System auto-detects blocks in range \u2014 no need to manually list (bN) placeholders. Just write a short prose summary."
|
|
5523
|
+
`- Compressed blocks: ${blockCount} (${blockList})`
|
|
5429
5524
|
];
|
|
5430
5525
|
if (includeHint) {
|
|
5431
|
-
lines.push("- \u{1F4A1}
|
|
5526
|
+
lines.push("- \u{1F4A1} Tools: compress, decompress, search_context.");
|
|
5432
5527
|
}
|
|
5433
5528
|
if (blockCount > 50) {
|
|
5434
5529
|
const oldBlockIds = activeBlockIds.slice(0, Math.max(0, blockCount - 20));
|
|
@@ -5458,8 +5553,6 @@ function buildCompressedBlockGuidance(state, gcConfig, context) {
|
|
|
5458
5553
|
lines.push(...targets);
|
|
5459
5554
|
lines.push(` System auto-detects blocks in range \u2014 no need to manually list (bN) placeholders. Just write a short prose summary.`);
|
|
5460
5555
|
}
|
|
5461
|
-
} else {
|
|
5462
|
-
lines.push(`- \u{1F500} You have ${blockCount} blocks \u2014 use compress to consolidate adjacent same-topic blocks.`);
|
|
5463
5556
|
}
|
|
5464
5557
|
}
|
|
5465
5558
|
const usageRatio = context?.currentTokens && context?.modelContextLimit ? context.currentTokens / context.modelContextLimit : 0;
|
|
@@ -5795,41 +5888,15 @@ function applyMessageModeAnchoredNudge(anchorMessageIds, messages, baseNudgeText
|
|
|
5795
5888
|
injectAnchoredNudge(message, nudgeText);
|
|
5796
5889
|
}
|
|
5797
5890
|
}
|
|
5798
|
-
function
|
|
5799
|
-
if (threshold === void 0) return void 0;
|
|
5800
|
-
if (typeof threshold === "number") {
|
|
5801
|
-
if (!modelContextLimit) return void 0;
|
|
5802
|
-
return threshold / modelContextLimit * 100;
|
|
5803
|
-
}
|
|
5804
|
-
const parsed = parseFloat(threshold);
|
|
5805
|
-
return isNaN(parsed) ? void 0 : parsed;
|
|
5806
|
-
}
|
|
5807
|
-
function buildContextUsageGuidance(config, currentTokens, modelContextLimit, minimal = false) {
|
|
5891
|
+
function buildContextUsageGuidance(config, currentTokens, modelContextLimit) {
|
|
5808
5892
|
if (currentTokens === void 0 || modelContextLimit === void 0 || modelContextLimit === 0) {
|
|
5809
5893
|
return "";
|
|
5810
5894
|
}
|
|
5811
|
-
const pct = currentTokens / modelContextLimit * 100;
|
|
5812
|
-
const percentage = pct.toFixed(1);
|
|
5813
5895
|
const formatK = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
5814
|
-
const minPct = resolveThresholdPercent(config.compress.minContextLimit, modelContextLimit) ?? 45;
|
|
5815
|
-
const maxPct = resolveThresholdPercent(config.compress.maxContextLimit, modelContextLimit) ?? 55;
|
|
5816
|
-
const base = `Context usage: ${formatK(currentTokens)} / ${formatK(modelContextLimit)} tokens (${percentage}%).`;
|
|
5817
|
-
if (minimal) {
|
|
5818
|
-
return `
|
|
5819
|
-
|
|
5820
|
-
${base}`;
|
|
5821
|
-
}
|
|
5822
|
-
let guidance;
|
|
5823
|
-
if (pct < minPct) {
|
|
5824
|
-
guidance = " \u{1F4A1} Be frugal with context. If any visible tool output exceeds 5000 characters and you've finished reading it, compress it into a summary now \u2014 don't keep large outputs 'just in case'. You can decompress later if needed.";
|
|
5825
|
-
} else if (pct < maxPct) {
|
|
5826
|
-
guidance = " \u26A0\uFE0F Context is growing \u2014 compress completed sections and high-token waste now.";
|
|
5827
|
-
} else {
|
|
5828
|
-
guidance = " \u{1F525} Context is high \u2014 compress aggressively, preserve only what is essential.";
|
|
5829
|
-
}
|
|
5830
5896
|
return `
|
|
5831
5897
|
|
|
5832
|
-
${
|
|
5898
|
+
Context: ${formatK(currentTokens)} tokens.
|
|
5899
|
+
All compression serves the primary task, but be frugal. Context capacity is precious \u2014 compress waste promptly. Save context by compressing consumed outputs, not by avoiding tools. Compress by need, not by percentage.`;
|
|
5833
5900
|
}
|
|
5834
5901
|
function applyAnchoredNudges(state, config, messages, prompts, compressionPriorities, currentTokens, modelContextLimit, suffixMessage) {
|
|
5835
5902
|
const contextUsageInfo = buildContextUsageGuidance(config, currentTokens, modelContextLimit);
|
|
@@ -5923,18 +5990,6 @@ function createSuffixMessage(messages) {
|
|
|
5923
5990
|
messages.push(synthetic);
|
|
5924
5991
|
return synthetic;
|
|
5925
5992
|
}
|
|
5926
|
-
function shouldInjectPerMessageNudge(state, config, currentTokens, modelContextLimit) {
|
|
5927
|
-
const turn = state.currentTurn ?? 0;
|
|
5928
|
-
const lastTurn = state.nudges.lastPerMessageNudgeTurn ?? 0;
|
|
5929
|
-
const turnsSinceLast = turn - lastTurn;
|
|
5930
|
-
const tokens = currentTokens ?? 0;
|
|
5931
|
-
const lastTokens = state.nudges.lastPerMessageNudgeTokens ?? 0;
|
|
5932
|
-
const tokenGrowth = tokens - lastTokens;
|
|
5933
|
-
const tokenGrowthPercent = modelContextLimit ? tokenGrowth / modelContextLimit * 100 : 0;
|
|
5934
|
-
const frequency = config.compress.nudgeFrequency ?? 5;
|
|
5935
|
-
const growthThreshold = config.compress.perMessageNudgeGrowthPercent ?? 3;
|
|
5936
|
-
return turnsSinceLast >= frequency || tokenGrowthPercent >= growthThreshold;
|
|
5937
|
-
}
|
|
5938
5993
|
var injectCompressNudges = (state, config, logger, messages, prompts, compressionPriorities) => {
|
|
5939
5994
|
if (compressPermission(state, config) === "deny") {
|
|
5940
5995
|
return;
|
|
@@ -5948,6 +6003,7 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
5948
6003
|
state.nudges.contextLimitAnchors.clear();
|
|
5949
6004
|
state.nudges.turnNudgeAnchors.clear();
|
|
5950
6005
|
state.nudges.iterationNudgeAnchors.clear();
|
|
6006
|
+
state.nudges.lastPerMessageNudgeTokens = 0;
|
|
5951
6007
|
void saveSessionState(state, logger);
|
|
5952
6008
|
return;
|
|
5953
6009
|
}
|
|
@@ -6019,8 +6075,24 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
6019
6075
|
}
|
|
6020
6076
|
const suffixMessage = createSuffixMessage(messages);
|
|
6021
6077
|
applyAnchoredNudges(state, config, messages, prompts, compressionPriorities, currentTokens, modelContextLimit, suffixMessage);
|
|
6022
|
-
const
|
|
6023
|
-
|
|
6078
|
+
const contextPct = modelContextLimit && currentTokens ? currentTokens / modelContextLimit * 100 : 0;
|
|
6079
|
+
const minPercent = config.compress?.minNudgeContextPercent ?? 15;
|
|
6080
|
+
injectContextUsage(suffixMessage, config, currentTokens, modelContextLimit);
|
|
6081
|
+
let tipsText = null;
|
|
6082
|
+
if (overMaxLimit || overMinLimit) {
|
|
6083
|
+
const lastWarnPct = state.nudges.lastPerMessageNudgeTokens && modelContextLimit ? state.nudges.lastPerMessageNudgeTokens / modelContextLimit * 100 : 0;
|
|
6084
|
+
const growthSinceWarn = contextPct - lastWarnPct;
|
|
6085
|
+
if (lastWarnPct === 0 || growthSinceWarn >= 10) {
|
|
6086
|
+
tipsText = overMaxLimit ? '\n\n\u26A0\uFE0F Context limit reached \u2014 compress now. Prioritize consumed tool outputs.\n\n{ "topic": "...", "content": [{ "startId": "<ID>", "endId": "<ID>", "summary": "..." }] }\n\nOnly use IDs from visible messages above. Compress older work first.' : "\n\n\u26A0\uFE0F Context is growing \u2014 consider compressing older work. Tools: compress, decompress, search_context.";
|
|
6087
|
+
state.nudges.lastPerMessageNudgeTokens = currentTokens ?? 0;
|
|
6088
|
+
state.nudges.lastPerMessageNudgeTurn = state.currentTurn ?? 0;
|
|
6089
|
+
}
|
|
6090
|
+
} else if (contextPct >= minPercent) {
|
|
6091
|
+
tipsText = "\n\n\u{1F4A1} Tools: compress, decompress, search_context.";
|
|
6092
|
+
if (state.nudges.lastPerMessageNudgeTokens) {
|
|
6093
|
+
state.nudges.lastPerMessageNudgeTokens = 0;
|
|
6094
|
+
}
|
|
6095
|
+
}
|
|
6024
6096
|
if (config.compress.mode !== "message") {
|
|
6025
6097
|
const visibleMessageIds = new Set(
|
|
6026
6098
|
messages.map((message) => message.info.id)
|
|
@@ -6028,26 +6100,29 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
6028
6100
|
const blockGuidance = buildCompressedBlockGuidance(state, config.gc, {
|
|
6029
6101
|
currentTokens,
|
|
6030
6102
|
modelContextLimit,
|
|
6031
|
-
includeHint:
|
|
6103
|
+
includeHint: tipsText !== null,
|
|
6032
6104
|
visibleMessageIds
|
|
6033
6105
|
});
|
|
6034
6106
|
if (blockGuidance.trim() && suffixMessage) {
|
|
6035
6107
|
appendToLastTextPart(suffixMessage, "\n\n" + blockGuidance);
|
|
6036
6108
|
}
|
|
6037
6109
|
}
|
|
6038
|
-
if (
|
|
6039
|
-
|
|
6040
|
-
state.nudges.lastPerMessageNudgeTokens = currentTokens ?? 0;
|
|
6110
|
+
if (tipsText && suffixMessage) {
|
|
6111
|
+
appendToLastTextPart(suffixMessage, tipsText);
|
|
6041
6112
|
}
|
|
6042
6113
|
injectVisibleIdRange(state, messages, suffixMessage);
|
|
6114
|
+
if (suffixMessage) {
|
|
6115
|
+
appendToLastTextPart(suffixMessage, "\n</acp-context>");
|
|
6116
|
+
}
|
|
6043
6117
|
if (anchorsChanged) {
|
|
6044
6118
|
void saveSessionState(state, logger);
|
|
6045
6119
|
}
|
|
6046
6120
|
};
|
|
6047
|
-
function injectContextUsage(target, config, currentTokens, modelContextLimit
|
|
6121
|
+
function injectContextUsage(target, config, currentTokens, modelContextLimit) {
|
|
6048
6122
|
if (!target) return;
|
|
6049
|
-
const
|
|
6050
|
-
if (!
|
|
6123
|
+
const rawUsage = buildContextUsageGuidance(config, currentTokens, modelContextLimit);
|
|
6124
|
+
if (!rawUsage) return;
|
|
6125
|
+
const usageTag = rawUsage;
|
|
6051
6126
|
for (const part of target.parts) {
|
|
6052
6127
|
if (part.type === "text") {
|
|
6053
6128
|
appendToTextPart(part, usageTag);
|
|
@@ -6071,7 +6146,7 @@ function injectVisibleIdRange(state, messages, target) {
|
|
|
6071
6146
|
const last = visibleRefs[visibleRefs.length - 1];
|
|
6072
6147
|
const rangeTag = `
|
|
6073
6148
|
|
|
6074
|
-
[Visible
|
|
6149
|
+
[Visible messages: ${first} to ${last} (${visibleRefs.length} messages)]`;
|
|
6075
6150
|
for (const part of target.parts) {
|
|
6076
6151
|
if (part.type === "text") {
|
|
6077
6152
|
appendToTextPart(part, rangeTag);
|
|
@@ -6495,11 +6570,12 @@ IMPORTANT:
|
|
|
6495
6570
|
- Do NOT call this tool in parallel with compress \u2014 their state mutations may conflict.`;
|
|
6496
6571
|
function buildSchema3() {
|
|
6497
6572
|
return {
|
|
6498
|
-
blockId:
|
|
6573
|
+
blockId: tool4.schema.string().describe('Block reference to decompress (e.g., "b0", "b2")'),
|
|
6574
|
+
toFile: tool4.schema.string().optional().describe("If provided, writes restored content to this file path instead of inflating context. Block stays compressed. Use read tool to access specific parts. Example: '/tmp/block52.txt'")
|
|
6499
6575
|
};
|
|
6500
6576
|
}
|
|
6501
6577
|
function createDecompressTool(ctx) {
|
|
6502
|
-
return
|
|
6578
|
+
return tool4({
|
|
6503
6579
|
description: TOOL_DESCRIPTION,
|
|
6504
6580
|
args: buildSchema3(),
|
|
6505
6581
|
async execute(args, toolCtx) {
|
|
@@ -6524,6 +6600,40 @@ function createDecompressTool(ctx) {
|
|
|
6524
6600
|
}
|
|
6525
6601
|
return `Error: Block ${target.displayId} is not active. It may have already been decompressed.`;
|
|
6526
6602
|
}
|
|
6603
|
+
if (args.toFile) {
|
|
6604
|
+
const targetPath = args.toFile;
|
|
6605
|
+
const os = await import("os");
|
|
6606
|
+
const path = await import("path");
|
|
6607
|
+
const allowedDirs = [
|
|
6608
|
+
os.tmpdir() + "/",
|
|
6609
|
+
path.join(os.homedir(), ".cache", "opencode") + "/"
|
|
6610
|
+
];
|
|
6611
|
+
const resolved = path.resolve(targetPath);
|
|
6612
|
+
const isAllowed = allowedDirs.some((dir) => {
|
|
6613
|
+
const rel = path.relative(dir, resolved);
|
|
6614
|
+
return rel === "" || !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
6615
|
+
});
|
|
6616
|
+
if (!isAllowed) {
|
|
6617
|
+
return `Error: toFile path must be under ${os.tmpdir()} or ~/.cache/opencode/. Got: ${targetPath}`;
|
|
6618
|
+
}
|
|
6619
|
+
const block = activeBlocks[0];
|
|
6620
|
+
const msgIds = new Set(block.effectiveMessageIds ?? []);
|
|
6621
|
+
const blockMessages = rawMessages.filter((m) => {
|
|
6622
|
+
const id = m.id ?? m.messageId ?? "";
|
|
6623
|
+
return msgIds.has(id);
|
|
6624
|
+
});
|
|
6625
|
+
const lines2 = blockMessages.map((m) => {
|
|
6626
|
+
const msg = m;
|
|
6627
|
+
const role = msg.role || msg.type || "unknown";
|
|
6628
|
+
const content = typeof msg.content === "string" ? msg.content : typeof msg.text === "string" ? msg.text : JSON.stringify(msg.content || msg.text || "");
|
|
6629
|
+
return `[${role}]
|
|
6630
|
+
${content}`;
|
|
6631
|
+
});
|
|
6632
|
+
const { writeFile: writeFile3 } = await import("fs/promises");
|
|
6633
|
+
const fileContent = lines2.length > 0 ? lines2.join("\n\n---\n\n") : block.summary ?? "(no content available)";
|
|
6634
|
+
await writeFile3(args.toFile, fileContent, "utf-8");
|
|
6635
|
+
return `Block b${target.displayId} content (${blockMessages.length} messages, ${fileContent.length} chars) written to ${args.toFile}. Block stays compressed \u2014 context unchanged. Use read tool to access specific parts.`;
|
|
6636
|
+
}
|
|
6527
6637
|
const activeMessagesBefore = snapshotActiveMessages(messagesState);
|
|
6528
6638
|
const activeBlockIdsBefore = new Set(messagesState.activeBlockIds);
|
|
6529
6639
|
deactivateCompressionTarget(messagesState, target);
|
|
@@ -6785,71 +6895,21 @@ var SYSTEM = `
|
|
|
6785
6895
|
|
|
6786
6896
|
You operate in a context-constrained environment. Context management helps preserve retrieval quality, but your primary goal is completing the task at hand. Do not let context management distract from the actual work.
|
|
6787
6897
|
|
|
6788
|
-
The tools you have for context management are \`compress\` and \`
|
|
6898
|
+
The tools you have for context management are \`compress\`, \`decompress\`, and \`search_context\`. \`compress\` replaces older conversation content with technical summaries you produce. \`decompress\` restores previously compressed content when you need exact details. \`search_context\` searches compressed block summaries (and visible messages) to locate relevant content before you decompress.
|
|
6789
6899
|
|
|
6790
|
-
\`<dcp-message-id>\` and \`<dcp-system-reminder>\` tags are
|
|
6900
|
+
\`<acp-context>\` tags wrap ACP (Agent Context Pruning) system metadata \u2014 context management information injected each turn. This is system data, not user input. You may also see \`<dcp-message-id>\` and \`<dcp-system-reminder>\` tags \u2014 these are equivalent (DCP was the previous name for ACP).
|
|
6791
6901
|
|
|
6792
6902
|
COMPRESSION PHILOSOPHY
|
|
6793
6903
|
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
Target the largest UNCOMPRESSED content first. Savings scale with original size \u2014 compressing a 5000-token tool output frees far more than re-shrinking an already-summarized 300-token block.
|
|
6799
|
-
|
|
6800
|
-
CONTEXT PRESSURE LEVELS
|
|
6801
|
-
|
|
6802
|
-
- Normal: Proactively compress finished outputs (agent results, verbose commands, large tool outputs) into summaries after you've finished using them. You can decompress later if needed.
|
|
6803
|
-
- Elevated: Context is growing \u2014 compress completed sections and high-token waste now.
|
|
6804
|
-
- Critical: Compress aggressively now \u2014 preserve only what is essential for the current task.
|
|
6805
|
-
|
|
6806
|
-
WHAT TO COMPRESS FIRST (high value, low risk)
|
|
6807
|
-
|
|
6808
|
-
- Agent/subagent review and consultation results: Prime compression targets when context pressure rises \u2014 the surrounding reasoning and tool-call chatter is typically the largest block of uncompressed content. Note: if the agent tool is in your protected list, its output is auto-preserved in the summary, so the savings come from the surrounding conversation, not the agent output itself. Compress once you have fully consumed the results (all recommended actions applied or recorded in files). Recover via \`decompress\` while the block is still active. Re-invoking the agent is a last resort \u2014 it is a fresh run, not a cache hit.
|
|
6809
|
-
- Verbose command output (build/test runs, git diff/log/status, publish logs, directory listings): Once you have read the result, compress. Keep only the verdict \u2014 pass/fail status, commit hash, version number, or count. For failures, keep the specific error messages and file/line references needed to act on them. The full output is reproducible by re-running the command.
|
|
6810
|
-
- Exploration that led nowhere (failed approaches, dead-end searches): Compress to a one-line note about what was tried and why it failed.
|
|
6811
|
-
- Redundant tool results (reading the same file multiple times, repeated status checks, exhausted search results): Keep only the most recent result.
|
|
6812
|
-
- Intermediate steps of completed multi-step tasks: Once the task is done, compress the process. Keep only the final outcome.
|
|
6813
|
-
- Resolved discussion threads (clarification rounds, negotiated requirements, design debate that reached a decision): Once a conclusion is recorded, compress the back-and-forth. Keep the decision and its rationale.
|
|
6814
|
-
- Large file contents that have already been used and are no longer needed: Compress to a summary of key functions, types, or patterns.
|
|
6815
|
-
|
|
6816
|
-
DO NOT RE-COMPRESS (low value, diminishing returns)
|
|
6817
|
-
|
|
6818
|
-
- Already-compressed block summaries: Re-compressing a summary into a shorter summary saves negligible tokens. If a block needs better detail, use \`decompress\` to restore it, then compress the original content properly. Exception: if a block-aging warning flags specific block IDs as facing GC truncation, re-summarize exactly those flagged blocks into a fresh range \u2014 this preserves detail that GC would otherwise destroy.
|
|
6819
|
-
- Short messages (1-3 sentences): The compression overhead (block metadata, summary structure) may exceed the tokens saved.
|
|
6820
|
-
- Content whose immediate use is complete \u2014 the task it supported is done and no open todo/plan references it. If still in active use, let it stay.
|
|
6821
|
-
- User instructions and requirements: These must remain visible until the task is complete.
|
|
6822
|
-
- Tool calls that are still pending or in-progress: Wait until the result is returned and consumed.
|
|
6823
|
-
|
|
6824
|
-
WHAT TO COMPRESS CAREFULLY (high risk - verify before compressing)
|
|
6825
|
-
|
|
6826
|
-
- Temporary secrets/keys/tokens needed later: Do NOT compress unless recorded elsewhere
|
|
6827
|
-
- File paths and directory structures: Keep in summary - losing these wastes tokens rediscovering them
|
|
6828
|
-
- Key function/method signatures and APIs: Summarize with exact names and signatures
|
|
6829
|
-
- Critical error messages and stack traces: Keep the error type and key detail in summary
|
|
6830
|
-
- User preferences and requirements: These must survive compression intact
|
|
6831
|
-
- Architectural decisions and rationale: Summarize the decision, not just the conclusion
|
|
6832
|
-
|
|
6833
|
-
BEFORE COMPRESSING IMPORTANT CONTENT
|
|
6834
|
-
|
|
6835
|
-
Verify the information is persisted in one of:
|
|
6836
|
-
- A file you have written or edited
|
|
6837
|
-
- An issue, PR, or devlog entry
|
|
6838
|
-
- The compression summary itself (include the critical bits explicitly)
|
|
6839
|
-
|
|
6840
|
-
If it is not persisted anywhere, either persist it first or include it explicitly in your compression summary.
|
|
6841
|
-
|
|
6842
|
-
AFTER COMPRESSING
|
|
6904
|
+
All compression serves the primary task, but be frugal. Two failure modes to avoid:
|
|
6905
|
+
- Over-compression: Compressing too aggressively loses critical details, decisions, and state needed for your task. This directly harms task quality.
|
|
6906
|
+
- Under-compression: Failing to compress verbose outputs causes context overflow, reducing accuracy and eventually blocking your work.
|
|
6843
6907
|
|
|
6844
|
-
|
|
6845
|
-
- Reference specific files by path
|
|
6846
|
-
- Include key variable names, function signatures, or configuration values
|
|
6847
|
-
- Note what was decided and why, not just what was done
|
|
6848
|
-
- Example: "Implemented auth check in src/middleware.ts using validateToken() from auth.ts - user table is users not user"
|
|
6908
|
+
Balance is key. Compress selectively to keep context lean. But never compress content you're actively using for an ongoing task. Use \`search_context\` to find compressed content when needed, and \`decompress\` to restore details.
|
|
6849
6909
|
|
|
6850
|
-
|
|
6910
|
+
BE FRUGAL
|
|
6851
6911
|
|
|
6852
|
-
|
|
6912
|
+
Be frugal with context \u2014 compress obvious waste promptly. Examples include verbose command output (build/test logs, git diff/status, npm install), sub-agent results once consumed, experiment/training logs (keep final metrics only), duplicate file reads, and failed explorations. Any content that is finished serving the task and would not be needed in upcoming turns should be compressed \u2014 not just these examples.
|
|
6853
6913
|
`;
|
|
6854
6914
|
|
|
6855
6915
|
// lib/prompts/compress-range.ts
|
|
@@ -7112,7 +7172,7 @@ var PROMPT_DEFINITIONS = [
|
|
|
7112
7172
|
];
|
|
7113
7173
|
var HTML_COMMENT_REGEX = /<!--[\s\S]*?-->/g;
|
|
7114
7174
|
var LEGACY_INLINE_COMMENT_LINE_REGEX = /^[ \t]*\/\/.*?\/\/[ \t]*$/gm;
|
|
7115
|
-
var DCP_SYSTEM_REMINDER_TAG_REGEX = /^\s*<dcp-system-reminder\b[^>]*>[\s\S]*<\/dcp-system-reminder>\s*$/i;
|
|
7175
|
+
var DCP_SYSTEM_REMINDER_TAG_REGEX = /^\s*<dcp-system-reminder\b[^>]*>[\s\S]*<\/(?:dcp|acp)-system-reminder>\s*$/i;
|
|
7116
7176
|
var DEFAULTS_README_FILE = "README.md";
|
|
7117
7177
|
var BUNDLED_EDITABLE_PROMPTS = {
|
|
7118
7178
|
system: SYSTEM,
|
|
@@ -7191,7 +7251,7 @@ function stripConditionalTag(content, tagName) {
|
|
|
7191
7251
|
function unwrapDcpTagIfWrapped(content) {
|
|
7192
7252
|
const trimmed = content.trim();
|
|
7193
7253
|
if (DCP_SYSTEM_REMINDER_TAG_REGEX.test(trimmed)) {
|
|
7194
|
-
return trimmed.replace(/^\s*<dcp-system-reminder\b[^>]*>\s*/i, "").replace(/\s*<\/dcp-system-reminder>\s*$/i, "").trim();
|
|
7254
|
+
return trimmed.replace(/^\s*<dcp-system-reminder\b[^>]*>\s*/i, "").replace(/\s*<\/(?:dcp|acp)-system-reminder>\s*$/i, "").trim();
|
|
7195
7255
|
}
|
|
7196
7256
|
return trimmed;
|
|
7197
7257
|
}
|
|
@@ -7201,7 +7261,7 @@ function normalizeReminderPromptContent(content) {
|
|
|
7201
7261
|
return "";
|
|
7202
7262
|
}
|
|
7203
7263
|
const startsWrapped = /^\s*<dcp-system-reminder\b[^>]*>/i.test(normalized);
|
|
7204
|
-
const endsWrapped = /<\/dcp-system-reminder>\s*$/i.test(normalized);
|
|
7264
|
+
const endsWrapped = /<\/(?:dcp|acp)-system-reminder>\s*$/i.test(normalized);
|
|
7205
7265
|
if (startsWrapped !== endsWrapped) {
|
|
7206
7266
|
return "";
|
|
7207
7267
|
}
|
|
@@ -7801,7 +7861,7 @@ var COMPRESS_TRIGGER_PROMPT = [
|
|
|
7801
7861
|
"Follow the active compress mode, preserve all critical implementation details, and choose safe targets.",
|
|
7802
7862
|
"Return after compress with a brief explanation of what content was compressed."
|
|
7803
7863
|
].join("\n\n");
|
|
7804
|
-
function getTriggerPrompt(
|
|
7864
|
+
function getTriggerPrompt(tool5, state, config, userFocus) {
|
|
7805
7865
|
const base = COMPRESS_TRIGGER_PROMPT;
|
|
7806
7866
|
const compressedBlockGuidance = config.compress.mode === "message" ? "" : buildCompressedBlockGuidance(state, config.gc);
|
|
7807
7867
|
const sections = [base, compressedBlockGuidance];
|
|
@@ -7830,8 +7890,8 @@ async function handleManualToggleCommand(ctx, modeArg) {
|
|
|
7830
7890
|
);
|
|
7831
7891
|
logger.info("Manual mode toggled", { manualMode: state.manualMode });
|
|
7832
7892
|
}
|
|
7833
|
-
async function handleManualTriggerCommand(ctx,
|
|
7834
|
-
return getTriggerPrompt(
|
|
7893
|
+
async function handleManualTriggerCommand(ctx, tool5, userFocus) {
|
|
7894
|
+
return getTriggerPrompt(tool5, ctx.state, ctx.config, userFocus);
|
|
7835
7895
|
}
|
|
7836
7896
|
function applyPendingManualTrigger(state, messages, logger) {
|
|
7837
7897
|
const pending = state.pendingManualTrigger;
|
|
@@ -9091,7 +9151,8 @@ var server = (async (ctx) => {
|
|
|
9091
9151
|
tool: {
|
|
9092
9152
|
...config.compress.permission !== "deny" && {
|
|
9093
9153
|
compress: config.compress.mode === "message" ? createCompressMessageTool(compressToolContext) : createCompressRangeTool(compressToolContext),
|
|
9094
|
-
decompress: createDecompressTool(compressToolContext)
|
|
9154
|
+
decompress: createDecompressTool(compressToolContext),
|
|
9155
|
+
search_context: createSearchContextTool(compressToolContext)
|
|
9095
9156
|
}
|
|
9096
9157
|
},
|
|
9097
9158
|
config: async (opencodeConfig) => {
|
|
@@ -9107,7 +9168,7 @@ var server = (async (ctx) => {
|
|
|
9107
9168
|
}
|
|
9108
9169
|
const toolsToAdd = [];
|
|
9109
9170
|
if (config.compress.permission !== "deny" && !config.experimental.allowSubAgents) {
|
|
9110
|
-
toolsToAdd.push("compress", "decompress");
|
|
9171
|
+
toolsToAdd.push("compress", "decompress", "search_context");
|
|
9111
9172
|
}
|
|
9112
9173
|
if (toolsToAdd.length > 0) {
|
|
9113
9174
|
const existingPrimaryTools = opencodeConfig.experimental?.primary_tools ?? [];
|