@lexwdex-org/opencode-dcp 3.3.4 → 3.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -862,6 +862,37 @@ var ParseErrorCode;
862
862
  ParseErrorCode2[ParseErrorCode2["InvalidCharacter"] = 16] = "InvalidCharacter";
863
863
  })(ParseErrorCode || (ParseErrorCode = {}));
864
864
 
865
+ // lib/config-env-override.ts
866
+ function applyExternalModelEnvOverride(config) {
867
+ const envUrl = process.env.OPENCODE_DCP_EXTERNAL_COMPRESS_URL;
868
+ const envApiKey = process.env.OPENCODE_DCP_EXTERNAL_COMPRESS_KEY;
869
+ const envModel = process.env.OPENCODE_DCP_EXTERNAL_COMPRESS_MODEL;
870
+ const envTimeout = process.env.OPENCODE_DCP_EXTERNAL_COMPRESS_TIMEOUT;
871
+ const envRetries = process.env.OPENCODE_DCP_EXTERNAL_COMPRESS_RETRIES;
872
+ if (envUrl && envModel) {
873
+ const externalModel = {
874
+ url: envUrl,
875
+ model: envModel
876
+ };
877
+ if (envApiKey) {
878
+ externalModel.apiKey = envApiKey;
879
+ }
880
+ if (envTimeout) {
881
+ const parsed = parseInt(envTimeout, 10);
882
+ if (parsed > 0) {
883
+ externalModel.timeout = parsed;
884
+ }
885
+ }
886
+ if (envRetries) {
887
+ const parsed = parseInt(envRetries, 10);
888
+ if (parsed >= 0) {
889
+ externalModel.retries = parsed;
890
+ }
891
+ }
892
+ config.compress.externalModel = externalModel;
893
+ }
894
+ }
895
+
865
896
  // lib/config.ts
866
897
  var DEFAULT_PROTECTED_TOOLS = [
867
898
  "task",
@@ -912,6 +943,7 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
912
943
  "compress.protectedTools",
913
944
  "compress.protectTags",
914
945
  "compress.protectUserMessages",
946
+ "compress.externalModel",
915
947
  "strategies",
916
948
  "strategies.deduplication",
917
949
  "strategies.deduplication.enabled",
@@ -1213,6 +1245,30 @@ function validateConfigTypes(config) {
1213
1245
  actual: JSON.stringify(compress.permission)
1214
1246
  });
1215
1247
  }
1248
+ if (compress.externalModel !== void 0) {
1249
+ if (typeof compress.externalModel !== "object" || compress.externalModel === null || Array.isArray(compress.externalModel)) {
1250
+ errors.push({
1251
+ key: "compress.externalModel",
1252
+ expected: "object with url and model",
1253
+ actual: typeof compress.externalModel
1254
+ });
1255
+ } else {
1256
+ if (typeof compress.externalModel.url !== "string") {
1257
+ errors.push({
1258
+ key: "compress.externalModel.url",
1259
+ expected: "string",
1260
+ actual: typeof compress.externalModel.url
1261
+ });
1262
+ }
1263
+ if (typeof compress.externalModel.model !== "string") {
1264
+ errors.push({
1265
+ key: "compress.externalModel.model",
1266
+ expected: "string",
1267
+ actual: typeof compress.externalModel.model
1268
+ });
1269
+ }
1270
+ }
1271
+ }
1216
1272
  if (compress.showCompression !== void 0 && typeof compress.showCompression !== "boolean") {
1217
1273
  errors.push({
1218
1274
  key: "compress.showCompression",
@@ -1342,7 +1398,8 @@ var defaultConfig = {
1342
1398
  nudgeForce: "strong",
1343
1399
  protectedTools: [...COMPRESS_DEFAULT_PROTECTED_TOOLS],
1344
1400
  protectTags: false,
1345
- protectUserMessages: false
1401
+ protectUserMessages: false,
1402
+ externalModel: void 0
1346
1403
  },
1347
1404
  strategies: {
1348
1405
  deduplication: {
@@ -1465,7 +1522,8 @@ function mergeCompress(base, override) {
1465
1522
  nudgeForce: override.nudgeForce ?? base.nudgeForce,
1466
1523
  protectedTools: [.../* @__PURE__ */ new Set([...base.protectedTools, ...override.protectedTools ?? []])],
1467
1524
  protectTags: override.protectTags ?? base.protectTags,
1468
- protectUserMessages: override.protectUserMessages ?? base.protectUserMessages
1525
+ protectUserMessages: override.protectUserMessages ?? base.protectUserMessages,
1526
+ externalModel: override.externalModel ?? base.externalModel
1469
1527
  };
1470
1528
  }
1471
1529
  function mergeCommands(base, override) {
@@ -1509,7 +1567,8 @@ function deepCloneConfig(config) {
1509
1567
  ...config.compress,
1510
1568
  modelMaxLimits: { ...config.compress.modelMaxLimits },
1511
1569
  modelMinLimits: { ...config.compress.modelMinLimits },
1512
- protectedTools: [...config.compress.protectedTools]
1570
+ protectedTools: [...config.compress.protectedTools],
1571
+ externalModel: config.compress.externalModel ? { ...config.compress.externalModel } : void 0
1513
1572
  },
1514
1573
  strategies: {
1515
1574
  deduplication: {
@@ -1591,6 +1650,7 @@ Using previous/default values`
1591
1650
  showConfigWarnings(ctx, layer.path, result.data, layer.isProject);
1592
1651
  config = mergeLayer(config, result.data);
1593
1652
  }
1653
+ applyExternalModelEnvOverride(config);
1594
1654
  return config;
1595
1655
  }
1596
1656
 
@@ -1793,7 +1853,12 @@ function countAllMessageTokens(msg) {
1793
1853
  }
1794
1854
 
1795
1855
  // lib/prompts/extensions/tool.ts
1796
- var RANGE_FORMAT_EXTENSION = `
1856
+ function buildRangeFormatExtension(externalModelEnabled) {
1857
+ const summaryField = externalModelEnabled ? `summary?: string // \u66FF\u6362\u8303\u56F4\u5185\u6240\u6709\u5185\u5BB9\u7684\u5B8C\u6574\u6280\u672F\u6458\u8981\uFF08\u53EF\u7701\u7565\uFF1B\u5916\u90E8\u6A21\u578B\u751F\u6210\uFF09` : `summary: string // \u66FF\u6362\u8303\u56F4\u5185\u6240\u6709\u5185\u5BB9\u7684\u5B8C\u6574\u6280\u672F\u6458\u8981`;
1858
+ const externalHint = externalModelEnabled ? `
1859
+
1860
+ \u5F53\u542F\u7528\u5916\u90E8\u6A21\u578B\u538B\u7F29\u65F6\uFF0Csummary \u5B57\u6BB5\u53EF\u7701\u7565\uFF1B\u63D2\u4EF6\u5C06\u81EA\u52A8\u8C03\u7528\u5916\u90E8\u6A21\u578B\u751F\u6210\u3002` : "";
1861
+ return `
1797
1862
  \u538B\u7F29\u683C\u5F0F
1798
1863
 
1799
1864
  \`\`\`
@@ -1803,12 +1868,20 @@ var RANGE_FORMAT_EXTENSION = `
1803
1868
  {
1804
1869
  startId: string, // \u8303\u56F4\u5F00\u59CB\u7684\u8FB9\u754C ID\uFF1AmNNNN \u6216 bN
1805
1870
  endId: string, // \u8303\u56F4\u7ED3\u675F\u7684\u8FB9\u754C ID\uFF1AmNNNN \u6216 bN
1806
- summary: string // \u66FF\u6362\u8303\u56F4\u5185\u6240\u6709\u5185\u5BB9\u7684\u5B8C\u6574\u6280\u672F\u6458\u8981
1871
+ ${summaryField}
1807
1872
  }
1808
1873
  ]
1809
1874
  }
1810
- \`\`\``;
1811
- var MESSAGE_FORMAT_EXTENSION = `
1875
+ \`\`\`
1876
+
1877
+ \u8FB9\u754C ID \u5FC5\u987B\u4ECE\u5F53\u524D\u53EF\u89C1\u4E0A\u4E0B\u6587\u4E2D\u5DF2\u6709\u7684\u6CE8\u5165 ID \u7CBE\u786E\u9009\u53D6\uFF1B\u4E0D\u8981\u53D1\u660E ID\uFF0C\u4E5F\u4E0D\u8981\u6839\u636E\u987A\u5E8F\u5916\u63A8\u4E0D\u5B58\u5728\u7684 ID\u3002${externalHint}`;
1878
+ }
1879
+ function buildMessageFormatExtension(externalModelEnabled) {
1880
+ const summaryField = externalModelEnabled ? `summary?: string // \u66FF\u6362\u8BE5\u6761\u6D88\u606F\u7684\u5B8C\u6574\u6280\u672F\u6458\u8981\uFF08\u53EF\u7701\u7565\uFF1B\u5916\u90E8\u6A21\u578B\u751F\u6210\uFF09` : `summary: string // \u66FF\u6362\u8BE5\u6761\u6D88\u606F\u7684\u5B8C\u6574\u6280\u672F\u6458\u8981`;
1881
+ const externalHint = externalModelEnabled ? `
1882
+
1883
+ \u5F53\u542F\u7528\u5916\u90E8\u6A21\u578B\u538B\u7F29\u65F6\uFF0Csummary \u5B57\u6BB5\u53EF\u7701\u7565\uFF1B\u63D2\u4EF6\u5C06\u81EA\u52A8\u8C03\u7528\u5916\u90E8\u6A21\u578B\u751F\u6210\u3002` : "";
1884
+ return `
1812
1885
  \u538B\u7F29\u683C\u5F0F
1813
1886
 
1814
1887
  \`\`\`
@@ -1818,11 +1891,14 @@ var MESSAGE_FORMAT_EXTENSION = `
1818
1891
  {
1819
1892
  messageId: string, // \u4EC5\u539F\u59CB\u6D88\u606F ID\uFF1AmNNNN\uFF08\u5FFD\u7565 priority \u7B49\u5143\u6570\u636E\u5C5E\u6027\uFF09
1820
1893
  topic: string, // \u6B64\u5355\u6761\u6D88\u606F\u6458\u8981\u7684\u77ED\u6807\u7B7E\uFF083-5 \u4E2A\u8BCD\uFF09
1821
- summary: string // \u66FF\u6362\u8BE5\u6761\u6D88\u606F\u7684\u5B8C\u6574\u6280\u672F\u6458\u8981
1894
+ ${summaryField}
1822
1895
  }
1823
1896
  ]
1824
1897
  }
1825
- \`\`\``;
1898
+ \`\`\`${externalHint}`;
1899
+ }
1900
+ var RANGE_FORMAT_EXTENSION = buildRangeFormatExtension(false);
1901
+ var MESSAGE_FORMAT_EXTENSION = buildMessageFormatExtension(false);
1826
1902
 
1827
1903
  // lib/message-ids.ts
1828
1904
  var MESSAGE_REF_REGEX = /^m(\d{4})$/;
@@ -1985,11 +2061,17 @@ function resolveBoundaryIds(context, state, startId, endId) {
1985
2061
  const issues = [];
1986
2062
  const parsedStartId = parseBoundaryId(startId);
1987
2063
  const parsedEndId = parseBoundaryId(endId);
2064
+ const hint = formatAvailableBoundaryHint(lookup);
2065
+ const guidance = formatBoundaryUsageGuidance();
1988
2066
  if (parsedStartId === null) {
1989
- issues.push("startId is invalid. Use an injected message ID (mNNNN) or block ID (bN).");
2067
+ issues.push(
2068
+ `startId is invalid. Use an injected message ID (mNNNN) or block ID (bN).${hint}${guidance}`
2069
+ );
1990
2070
  }
1991
2071
  if (parsedEndId === null) {
1992
- issues.push("endId is invalid. Use an injected message ID (mNNNN) or block ID (bN).");
2072
+ issues.push(
2073
+ `endId is invalid. Use an injected message ID (mNNNN) or block ID (bN).${hint}${guidance}`
2074
+ );
1993
2075
  }
1994
2076
  if (issues.length > 0) {
1995
2077
  throw new Error(
@@ -2002,28 +2084,18 @@ function resolveBoundaryIds(context, state, startId, endId) {
2002
2084
  const startReference = lookup.get(parsedStartId.ref);
2003
2085
  const endReference = lookup.get(parsedEndId.ref);
2004
2086
  if (!startReference || !endReference) {
2005
- const allKeys = Array.from(lookup.keys());
2006
- const availableBlockRefs = allKeys.filter((key) => key.startsWith("b")).sort(
2007
- (a, b) => Number.parseInt(a.slice(1), 10) - Number.parseInt(b.slice(1), 10)
2008
- );
2009
- const availableMsgRefs = allKeys.filter((key) => key.startsWith("m")).sort(
2010
- (a, b) => Number.parseInt(a.slice(1), 10) - Number.parseInt(b.slice(1), 10)
2011
- );
2012
- const blockHint = availableBlockRefs.length ? ` Available block IDs: ${availableBlockRefs.join(", ")}.` : " No block IDs available.";
2013
- const msgHint = availableMsgRefs.length === 0 ? " No message IDs available." : availableMsgRefs.length <= 10 ? ` Available message IDs: ${availableMsgRefs.join(", ")}.` : ` Available message IDs: ${availableMsgRefs[0]} to ${availableMsgRefs[availableMsgRefs.length - 1]} (${availableMsgRefs.length} total).`;
2014
- const hint = `${msgHint}${blockHint}`;
2015
2087
  if (!startReference) {
2016
2088
  const wasKnown = state.messageIds.byRef.has(parsedStartId.ref);
2017
2089
  const reason = wasKnown ? " (message was likely compacted out of the conversation)" : " (invalid ID)";
2018
2090
  issues.push(
2019
- `startId ${parsedStartId.ref} is not available${reason}. Choose an injected ID visible in context.${hint}`
2091
+ `startId ${parsedStartId.ref} is not available${reason}. Choose an injected ID visible in context.${hint}${guidance}`
2020
2092
  );
2021
2093
  }
2022
2094
  if (!endReference) {
2023
2095
  const wasKnown = state.messageIds.byRef.has(parsedEndId.ref);
2024
2096
  const reason = wasKnown ? " (message was likely compacted out of the conversation)" : " (invalid ID)";
2025
2097
  issues.push(
2026
- `endId ${parsedEndId.ref} is not available${reason}. Choose an injected ID visible in context.${hint}`
2098
+ `endId ${parsedEndId.ref} is not available${reason}. Choose an injected ID visible in context.${hint}${guidance}`
2027
2099
  );
2028
2100
  }
2029
2101
  }
@@ -2129,6 +2201,17 @@ function resolveAnchorMessageId(startReference) {
2129
2201
  }
2130
2202
  return startReference.messageId;
2131
2203
  }
2204
+ function formatAvailableBoundaryHint(lookup) {
2205
+ const allKeys = Array.from(lookup.keys());
2206
+ const availableBlockRefs = allKeys.filter((key) => key.startsWith("b")).sort((a, b) => Number.parseInt(a.slice(1), 10) - Number.parseInt(b.slice(1), 10));
2207
+ const availableMsgRefs = allKeys.filter((key) => key.startsWith("m")).sort((a, b) => Number.parseInt(a.slice(1), 10) - Number.parseInt(b.slice(1), 10));
2208
+ const blockHint = availableBlockRefs.length ? ` Available block IDs: ${availableBlockRefs.join(", ")}.` : " No block IDs available.";
2209
+ const msgHint = availableMsgRefs.length === 0 ? " No message IDs available." : availableMsgRefs.length <= 10 ? ` Available message IDs: ${availableMsgRefs.join(", ")}.` : ` Available message IDs: ${availableMsgRefs[0]} to ${availableMsgRefs[availableMsgRefs.length - 1]} (${availableMsgRefs.length} total).`;
2210
+ return `${msgHint}${blockHint}`;
2211
+ }
2212
+ function formatBoundaryUsageGuidance() {
2213
+ return " Use exactly one of the listed injected IDs. For already compressed content, use its bN block ID.";
2214
+ }
2132
2215
  function buildBoundaryLookup(context, state) {
2133
2216
  const lookup = /* @__PURE__ */ new Map();
2134
2217
  for (const [messageRef, messageId] of state.messageIds.byRef) {
@@ -2446,8 +2529,10 @@ function validateArgs(args) {
2446
2529
  if (typeof entry?.topic !== "string" || entry.topic.trim().length === 0) {
2447
2530
  throw new Error(`${prefix}.topic is required and must be a non-empty string`);
2448
2531
  }
2449
- if (typeof entry?.summary !== "string" || entry.summary.trim().length === 0) {
2450
- throw new Error(`${prefix}.summary is required and must be a non-empty string`);
2532
+ if (entry?.summary !== void 0) {
2533
+ if (typeof entry.summary !== "string" || entry.summary.trim().length === 0) {
2534
+ throw new Error(`${prefix}.summary must be a non-empty string when provided`);
2535
+ }
2451
2536
  }
2452
2537
  }
2453
2538
  }
@@ -4165,8 +4250,118 @@ ${output}`);
4165
4250
  return summary + heading + protectedOutputs.join("");
4166
4251
  }
4167
4252
 
4253
+ // lib/compress/external-inference.ts
4254
+ var ExternalModelError = class extends Error {
4255
+ constructor(message, kind, cause) {
4256
+ super(message);
4257
+ this.kind = kind;
4258
+ this.cause = cause;
4259
+ this.name = "ExternalModelError";
4260
+ }
4261
+ };
4262
+ function resolveCompressBaseUrl(url) {
4263
+ const normalized = url.replace(/\/$/, "");
4264
+ if (normalized.endsWith("/chat/completions")) {
4265
+ return normalized;
4266
+ }
4267
+ return `${normalized}/chat/completions`;
4268
+ }
4269
+ async function callExternalModel(endpoint, cfg, req) {
4270
+ const controller = new AbortController();
4271
+ const timeoutMs = cfg.timeout ?? 12e4;
4272
+ const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
4273
+ let response;
4274
+ try {
4275
+ response = await fetch(endpoint, {
4276
+ method: "POST",
4277
+ headers: {
4278
+ "Content-Type": "application/json",
4279
+ ...cfg.apiKey ? { Authorization: `Bearer ${cfg.apiKey}` } : {}
4280
+ },
4281
+ body: JSON.stringify({
4282
+ model: cfg.model,
4283
+ messages: [
4284
+ { role: "system", content: req.systemPrompt },
4285
+ { role: "user", content: req.userContent }
4286
+ ],
4287
+ temperature: 0
4288
+ }),
4289
+ signal: controller.signal
4290
+ });
4291
+ } catch (error) {
4292
+ if (error instanceof Error && error.name === "AbortError") {
4293
+ throw new ExternalModelError(
4294
+ `External model request timed out after ${timeoutMs}ms`,
4295
+ "timeout",
4296
+ error
4297
+ );
4298
+ }
4299
+ throw new ExternalModelError(
4300
+ `External model request failed: ${error instanceof Error ? error.message : String(error)}`,
4301
+ "network",
4302
+ error
4303
+ );
4304
+ } finally {
4305
+ clearTimeout(timeoutHandle);
4306
+ }
4307
+ if (!response.ok) {
4308
+ let errorBody = "";
4309
+ try {
4310
+ errorBody = await response.text();
4311
+ } catch {
4312
+ }
4313
+ throw new ExternalModelError(
4314
+ `External model returned HTTP ${response.status}: ${errorBody.slice(0, 200)}`,
4315
+ "http",
4316
+ { status: response.status, body: errorBody }
4317
+ );
4318
+ }
4319
+ let parsed;
4320
+ try {
4321
+ parsed = await response.json();
4322
+ } catch (error) {
4323
+ throw new ExternalModelError("External model returned invalid JSON", "parse", error);
4324
+ }
4325
+ const content = parsed?.choices?.[0]?.message?.content;
4326
+ if (typeof content !== "string" || content.trim().length === 0) {
4327
+ throw new ExternalModelError(
4328
+ "External model returned empty or missing summary content",
4329
+ "empty",
4330
+ parsed
4331
+ );
4332
+ }
4333
+ return content;
4334
+ }
4335
+ async function generateSummaryViaExternal(cfg, req) {
4336
+ const endpoint = resolveCompressBaseUrl(cfg.url);
4337
+ const maxRetries = cfg.retries ?? 1;
4338
+ let lastError;
4339
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
4340
+ try {
4341
+ return await callExternalModel(endpoint, cfg, req);
4342
+ } catch (error) {
4343
+ lastError = error;
4344
+ }
4345
+ }
4346
+ throw lastError;
4347
+ }
4348
+
4168
4349
  // lib/compress/message.ts
4169
- function buildSchema() {
4350
+ function extractMessageText(messageId, searchContext) {
4351
+ const msg = searchContext.rawMessagesById.get(messageId);
4352
+ if (!msg) return "";
4353
+ const parts = [];
4354
+ for (const part of msg.parts) {
4355
+ if (part.type === "text" && typeof part.text === "string") {
4356
+ parts.push(part.text);
4357
+ }
4358
+ }
4359
+ return parts.join("\n\n");
4360
+ }
4361
+ function buildSchema(externalModelEnabled) {
4362
+ const summaryField = externalModelEnabled ? tool.schema.string().optional().describe(
4363
+ "Complete technical summary replacing that one message (optional when external model is configured)"
4364
+ ) : tool.schema.string().describe("Complete technical summary replacing that one message");
4170
4365
  return {
4171
4366
  topic: tool.schema.string().describe(
4172
4367
  "Short label (3-5 words) for the overall batch - e.g., 'Closed Research Notes'"
@@ -4175,7 +4370,7 @@ function buildSchema() {
4175
4370
  tool.schema.object({
4176
4371
  messageId: tool.schema.string().describe("Raw message ID to compress (e.g. m0001)"),
4177
4372
  topic: tool.schema.string().describe("Short label (3-5 words) for this one message summary"),
4178
- summary: tool.schema.string().describe("Complete technical summary replacing that one message")
4373
+ summary: summaryField
4179
4374
  })
4180
4375
  ).describe("Batch of individual message summaries to create in one tool call")
4181
4376
  };
@@ -4183,9 +4378,10 @@ function buildSchema() {
4183
4378
  function createCompressMessageTool(ctx) {
4184
4379
  ctx.prompts.reload();
4185
4380
  const runtimePrompts = ctx.prompts.getRuntimePrompts();
4381
+ const externalModelEnabled = ctx.config.compress.externalModel !== void 0;
4186
4382
  return tool({
4187
- description: runtimePrompts.compressMessage + MESSAGE_FORMAT_EXTENSION,
4188
- args: buildSchema(),
4383
+ description: runtimePrompts.compressMessage + buildMessageFormatExtension(externalModelEnabled),
4384
+ args: buildSchema(externalModelEnabled),
4189
4385
  async execute(args, toolCtx) {
4190
4386
  const input = args;
4191
4387
  validateArgs(input);
@@ -4204,9 +4400,29 @@ function createCompressMessageTool(ctx) {
4204
4400
  if (plans.length === 0 && skippedCount > 0) {
4205
4401
  throw new Error(formatIssues(skippedIssues, skippedCount));
4206
4402
  }
4403
+ if (ctx.config.compress.externalModel) {
4404
+ for (const plan of plans) {
4405
+ if (plan.entry.summary === void 0) {
4406
+ const userContent = extractMessageText(plan.entry.messageId, searchContext);
4407
+ const generated = await generateSummaryViaExternal(
4408
+ ctx.config.compress.externalModel,
4409
+ {
4410
+ systemPrompt: runtimePrompts.compressMessage,
4411
+ userContent
4412
+ }
4413
+ );
4414
+ plan.entry.summary = generated;
4415
+ }
4416
+ }
4417
+ }
4207
4418
  const notifications = [];
4208
4419
  const preparedPlans = [];
4209
4420
  for (const plan of plans) {
4421
+ if (plan.entry.summary === void 0) {
4422
+ throw new Error(
4423
+ "\u7F3A\u5C11 summary\uFF1A\u672A\u914D\u7F6E\u5916\u90E8\u6A21\u578B\uFF0C\u8BF7\u63D0\u4F9B summary \u6216\u914D\u7F6E OPENCODE_DCP_EXTERNAL_COMPRESS_URL/MODEL"
4424
+ );
4425
+ }
4210
4426
  const summaryWithPromptInfo = appendProtectedPromptInfo(
4211
4427
  plan.entry.summary,
4212
4428
  plan.selection,
@@ -4287,8 +4503,10 @@ function validateArgs2(args) {
4287
4503
  if (typeof entry?.endId !== "string" || entry.endId.trim().length === 0) {
4288
4504
  throw new Error(`${prefix}.endId is required and must be a non-empty string`);
4289
4505
  }
4290
- if (typeof entry?.summary !== "string" || entry.summary.trim().length === 0) {
4291
- throw new Error(`${prefix}.summary is required and must be a non-empty string`);
4506
+ if (entry?.summary !== void 0) {
4507
+ if (typeof entry.summary !== "string" || entry.summary.trim().length === 0) {
4508
+ throw new Error(`${prefix}.summary must be a non-empty string when provided`);
4509
+ }
4292
4510
  }
4293
4511
  }
4294
4512
  }
@@ -4494,16 +4712,34 @@ ${right}`;
4494
4712
  }
4495
4713
 
4496
4714
  // lib/compress/range.ts
4497
- function buildSchema2() {
4715
+ function extractRangeText(selection, searchContext) {
4716
+ const parts = [];
4717
+ for (const messageId of selection.messageIds) {
4718
+ const msg = searchContext.rawMessagesById.get(messageId);
4719
+ if (!msg) continue;
4720
+ for (const part of msg.parts) {
4721
+ if (part.type === "text" && typeof part.text === "string") {
4722
+ parts.push(part.text);
4723
+ }
4724
+ }
4725
+ }
4726
+ return parts.join("\n\n");
4727
+ }
4728
+ function buildSchema2(externalModelEnabled) {
4729
+ const summaryField = externalModelEnabled ? tool2.schema.string().optional().describe(
4730
+ "Complete technical summary replacing all content in range (optional when external model is configured)"
4731
+ ) : tool2.schema.string().describe("Complete technical summary replacing all content in range");
4498
4732
  return {
4499
4733
  topic: tool2.schema.string().describe("Short label (3-5 words) for display - e.g., 'Auth System Exploration'"),
4500
4734
  content: tool2.schema.array(
4501
4735
  tool2.schema.object({
4502
4736
  startId: tool2.schema.string().describe(
4503
- "Message or block ID marking the beginning of range (e.g. m0001, b2)"
4737
+ "Injected message or block ID visible in the current context marking the beginning of the range. Do not invent IDs; use an exact visible mNNNN or bN boundary."
4738
+ ),
4739
+ endId: tool2.schema.string().describe(
4740
+ "Injected message or block ID visible in the current context marking the end of the range. Do not invent IDs; use an exact visible mNNNN or bN boundary."
4504
4741
  ),
4505
- endId: tool2.schema.string().describe("Message or block ID marking the end of range (e.g. m0012, b5)"),
4506
- summary: tool2.schema.string().describe("Complete technical summary replacing all content in range")
4742
+ summary: summaryField
4507
4743
  })
4508
4744
  ).describe(
4509
4745
  "One or more ranges to compress, each with start/end boundaries and a summary"
@@ -4513,9 +4749,10 @@ function buildSchema2() {
4513
4749
  function createCompressRangeTool(ctx) {
4514
4750
  ctx.prompts.reload();
4515
4751
  const runtimePrompts = ctx.prompts.getRuntimePrompts();
4752
+ const externalModelEnabled = ctx.config.compress.externalModel !== void 0;
4516
4753
  return tool2({
4517
- description: runtimePrompts.compressRange + RANGE_FORMAT_EXTENSION,
4518
- args: buildSchema2(),
4754
+ description: runtimePrompts.compressRange + buildRangeFormatExtension(externalModelEnabled),
4755
+ args: buildSchema2(externalModelEnabled),
4519
4756
  async execute(args, toolCtx) {
4520
4757
  const input = args;
4521
4758
  validateArgs2(input);
@@ -4527,10 +4764,30 @@ function createCompressRangeTool(ctx) {
4527
4764
  );
4528
4765
  const resolvedPlans = resolveRanges(input, searchContext, ctx.state);
4529
4766
  validateNonOverlapping(resolvedPlans);
4767
+ if (ctx.config.compress.externalModel) {
4768
+ for (const plan of resolvedPlans) {
4769
+ if (plan.entry.summary === void 0) {
4770
+ const userContent = extractRangeText(plan.selection, searchContext);
4771
+ const generated = await generateSummaryViaExternal(
4772
+ ctx.config.compress.externalModel,
4773
+ {
4774
+ systemPrompt: runtimePrompts.compressRange,
4775
+ userContent
4776
+ }
4777
+ );
4778
+ plan.entry.summary = generated;
4779
+ }
4780
+ }
4781
+ }
4530
4782
  const notifications = [];
4531
4783
  const preparedPlans = [];
4532
4784
  let totalCompressedMessages = 0;
4533
4785
  for (const plan of resolvedPlans) {
4786
+ if (plan.entry.summary === void 0) {
4787
+ throw new Error(
4788
+ "\u7F3A\u5C11 summary\uFF1A\u672A\u914D\u7F6E\u5916\u90E8\u6A21\u578B\uFF0C\u8BF7\u63D0\u4F9B summary \u6216\u914D\u7F6E OPENCODE_DCP_EXTERNAL_COMPRESS_URL/MODEL"
4789
+ );
4790
+ }
4534
4791
  const parsedPlaceholders = parseBlockPlaceholders(plan.entry.summary);
4535
4792
  const missingBlockIds = validateSummaryPlaceholders(
4536
4793
  parsedPlaceholders,
@@ -4882,44 +5139,51 @@ import { homedir as homedir4 } from "os";
4882
5139
 
4883
5140
  // lib/prompts/system.ts
4884
5141
  var SYSTEM = `
4885
- \u4F60\u5728\u4E00\u4E2A\u4E0A\u4E0B\u6587\u53D7\u9650\u7684\u73AF\u5883\u4E2D\u8FD0\u884C\u3002\u6301\u7EED\u7BA1\u7406\u4E0A\u4E0B\u6587\u4EE5\u907F\u514D\u5806\u79EF\u5E76\u4FDD\u6301\u68C0\u7D22\u8D28\u91CF\u3002\u9AD8\u6548\u7684\u4E0A\u4E0B\u6587\u7BA1\u7406\u5BF9\u4F60\u7684\u4EE3\u7406\u6027\u80FD\u81F3\u5173\u91CD\u8981\u3002
5142
+ You run in a context-limited environment. Manage context actively to avoid accumulation and keep retrieval quality high.
5143
+
5144
+ Your only context-management tool is \`compress\`. It replaces older conversation content with a technical summary you generate.
5145
+
5146
+ Hard constraints
5147
+ - Context management is done ONLY by calling \`compress\`. Never produce summaries in plain text.
5148
+ - NEVER output \`<summary>\` or \`<analysis>\` XML tags in text responses \u2014 they cause system errors.
5149
+ - If context needs compression, call \`compress\`. Do not write summaries inline.
5150
+ - Use Markdown headings (e.g. \`## Analysis\`, \`## Summary\`) to organize information. Never use XML tags.
4886
5151
 
4887
- \u4F60\u7528\u4E8E\u4E0A\u4E0B\u6587\u7BA1\u7406\u7684\u552F\u4E00\u5DE5\u5177\u662F \`compress\`\u3002\u5B83\u7528\u4F60\u751F\u6210\u7684\u6280\u672F\u6458\u8981\u66FF\u6362\u8F83\u65E7\u7684\u5BF9\u8BDD\u5185\u5BB9\u3002
5152
+ \`\` and \`\` are environment-injected metadata. Do not output them.
4888
5153
 
4889
- \u5173\u952E\u7EA6\u675F
4890
- - \u4E0A\u4E0B\u6587\u7BA1\u7406\u4EC5\u901A\u8FC7\u8C03\u7528 \`compress\` \u5DE5\u5177\u5B8C\u6210\u3002\u7EDD\u4E0D\u4EE5\u7EAF\u6587\u672C\u5F62\u5F0F\u751F\u6210\u6458\u8981\u3002
4891
- - **\u4E25\u683C\u7981\u6B62**\u5728\u6587\u672C\u54CD\u5E94\u4E2D\u8F93\u51FA \`<summary>\` \u6216 \`<analysis>\` XML \u6807\u7B7E\u3002\u8FD9\u4E9B\u6807\u7B7E\u4F1A\u5BFC\u81F4\u7CFB\u7EDF\u9519\u8BEF\u3002
4892
- - \u5982\u679C\u4F60\u8BA4\u4E3A\u4E0A\u4E0B\u6587\u9700\u8981\u538B\u7F29\uFF0C\u8C03\u7528 \`compress\` \u5DE5\u5177\u3002\u4E0D\u8981\u5185\u8054\u7F16\u5199\u6458\u8981\u3002
4893
- - \u6240\u6709\u8F93\u51FA\u5FC5\u987B\u4F7F\u7528**\u4E2D\u6587**\u3002
4894
- - \u5982\u679C\u9700\u8981\u5728\u6587\u672C\u4E2D\u7EC4\u7EC7\u4FE1\u606F\uFF0C\u4F7F\u7528 Markdown \u683C\u5F0F\uFF08\u5982 \`## \u5206\u6790\`\u3001\`## \u603B\u7ED3\`\uFF09\uFF0C\u7EDD\u4E0D\u4F7F\u7528 XML \u6807\u7B7E\u3002
5154
+ When to compress
4895
5155
 
4896
- \`\` \u548C \`\` \u6807\u7B7E\u662F\u73AF\u5883\u6CE8\u5165\u7684\u5143\u6570\u636E\u3002\u4E0D\u8981\u8F93\u51FA\u5B83\u4EEC\u3002
5156
+ HIGHEST-PRIORITY HARD TRIGGER \u2014 call \`compress\` immediately whenever the conversation TOPIC changes. Compress the previous topic's finished context into a summary.
4897
5157
 
4898
- \u538B\u7F29\u7684\u54F2\u5B66
4899
- \`compress\` \u5C06\u5BF9\u8BDD\u5185\u5BB9\u8F6C\u6362\u4E3A\u5BC6\u96C6\u3001\u9AD8\u4FDD\u771F\u7684\u6458\u8981\u3002\u8FD9\u4E0D\u662F\u6E05\u7406\u2014\u2014\u800C\u662F\u7ED3\u6676\u3002\u4F60\u7684\u6458\u8981\u6210\u4E3A\u6240\u53D1\u751F\u4E8B\u60C5\u7684\u6743\u5A01\u8BB0\u5F55\u3002
5158
+ A topic change is an objective, low-judgment signal, not a subjective call. Any of these counts:
4900
5159
 
4901
- \u5C06\u538B\u7F29\u89C6\u4E3A\u76F8\u53D8\uFF1A\u539F\u59CB\u63A2\u7D22\u53D8\u4E3A\u7CBE\u70BC\u7406\u89E3\u3002\u539F\u59CB\u4E0A\u4E0B\u6587\u5DF2\u5B8C\u6210\u5176\u4F7F\u547D\uFF1B\u4F60\u7684\u6458\u8981\u73B0\u5728\u627F\u8F7D\u8BE5\u7406\u89E3\u5411\u524D\u3002
5160
+ - The user raises a task or question unrelated to the previous one
5161
+ - The focus shifts from one module/file/feature to another
5162
+ - The goal shifts from explore/research \u2192 implement, or implement \u2192 debug/verify
5163
+ - A distinct problem is resolved and you move to the next
4902
5164
 
4903
- \u4F55\u65F6\u538B\u7F29
5165
+ When any occurs, compress the finished range of the previous topic at once. Do not wait. This is the single most effective way to keep context clear.
4904
5166
 
4905
- \u5F53\u4E00\u4E2A\u90E8\u5206\u771F\u6B63\u5173\u95ED\u4E14\u539F\u59CB\u5BF9\u8BDD\u5DF2\u5B8C\u6210\u5176\u4F7F\u547D\u65F6\uFF1A
5167
+ Also compress (when a section genuinely closes even without a topic change):
4906
5168
 
4907
- - \u7814\u7A76\u5DF2\u5B8C\u6210\u4E14\u53D1\u73B0\u5DF2\u660E\u786E
4908
- - \u5B9E\u73B0\u5DF2\u5B8C\u6210\u5E76\u9A8C\u8BC1
4909
- - \u63A2\u7D22\u5DF2\u7A77\u5C3D\u4E14\u6A21\u5F0F\u5DF2\u7406\u89E3
4910
- - \u6B7B\u80E1\u540C\u566A\u97F3\u53EF\u4EE5\u5728\u4E0D\u7B49\u5F85\u6574\u4E2A\u7AE0\u8282\u5173\u95ED\u7684\u60C5\u51B5\u4E0B\u88AB\u4E22\u5F03
5169
+ - Research is done and findings are settled
5170
+ - Implementation is complete and verified
5171
+ - Exploration is exhausted and patterns understood
5172
+ - Dead-end noise can be dropped without waiting for the whole section to close
4911
5173
 
4912
- \u4E0D\u8981\u538B\u7F29\u7684\u60C5\u51B5
5174
+ Do NOT compress when:
4913
5175
 
4914
- - \u539F\u59CB\u4E0A\u4E0B\u6587\u4ECD\u7136\u76F8\u5173\u4E14\u9700\u8981\u7528\u4E8E\u7F16\u8F91\u6216\u7CBE\u786E\u5F15\u7528
4915
- - \u76EE\u6807\u5185\u5BB9\u4ECD\u5728\u79EF\u6781\u8FDB\u884C\u4E2D
4916
- - \u4F60\u53EF\u80FD\u5728\u63A5\u4E0B\u6765\u7684\u6B65\u9AA4\u4E2D\u9700\u8981\u786E\u5207\u7684\u4EE3\u7801\u3001\u9519\u8BEF\u6D88\u606F\u6216\u6587\u4EF6\u5185\u5BB9
5176
+ - Raw context is still needed for editing or exact reference
5177
+ - The target work is actively in progress
5178
+ - You may need the exact code, error message, or file content in the next steps
4917
5179
 
4918
- \u538B\u7F29\u524D\u95EE\u81EA\u5DF1\uFF1A_"\u8FD9\u4E2A\u90E8\u5206\u73B0\u5728\u662F\u5426\u8DB3\u591F\u5173\u95ED\u4EE5\u6210\u4E3A\u4EC5\u6458\u8981\uFF1F"_
5180
+ Ask yourself before compressing: _"Is this section closed enough to be summary-only?"_
4919
5181
 
4920
- \u5B9A\u671F\u8BC4\u4F30\u5BF9\u8BDD\u7684\u4FE1\u566A\u6BD4\u3002\u6709\u610F\u8BC6\u5730\u4F7F\u7528 \`compress\` \u5E76\u63D0\u4F9B\u9AD8\u8D28\u91CF\u6458\u8981\u3002\u667A\u80FD\u5730\u4F18\u5148\u5904\u7406\u8FC7\u65F6\u5185\u5BB9\uFF0C\u4EE5\u7EF4\u62A4\u652F\u6301\u4F60\u4EE3\u7406\u80FD\u529B\u7684\u9AD8\u4FE1\u53F7\u4E0A\u4E0B\u6587\u7A97\u53E3\u3002
5182
+ If the answer is "the topic switched, the old content is no longer needed" \u2014 the answer is always yes.
4921
5183
 
4922
- \u4FDD\u6301\u9510\u5229\u3001\u9AD8\u8D28\u91CF\u7684\u4E0A\u4E0B\u6587\u7A97\u53E3\u4EE5\u83B7\u5F97\u6700\u4F73\u6027\u80FD\u662F\u4F60\u7684\u8D23\u4EFB\u3002
5184
+ Periodically assess the conversation's signal-to-noise ratio. Use \`compress\` deliberately and provide high-quality summaries. Prioritize stale content to maintain a high-signal context window.
5185
+
5186
+ Keeping a sharp, high-quality context window for best performance is your responsibility.
4923
5187
  `;
4924
5188
 
4925
5189
  // lib/prompts/compress-range.ts
@@ -5056,32 +5320,44 @@ var COMPRESS_MESSAGE = `\u5C06\u5BF9\u8BDD\u4E2D\u9009\u5B9A\u7684\u5355\u6761\u
5056
5320
 
5057
5321
  // lib/prompts/context-limit-nudge.ts
5058
5322
  var CONTEXT_LIMIT_NUDGE = `<dcp-system-reminder>
5059
- \u4E25\u91CD\u8B66\u544A\uFF1A\u5DF2\u8FBE\u5230\u6700\u5927\u4E0A\u4E0B\u6587\u9650\u5236
5323
+ CRITICAL: max context limit reached.
5060
5324
 
5061
- \u4F60\u5F53\u524D\u5904\u4E8E\u6216\u5DF2\u8D85\u8FC7\u914D\u7F6E\u7684\u6700\u5927\u4E0A\u4E0B\u6587\u9608\u503C\u3002\u8FD9\u662F\u7D27\u6025\u7684\u4E0A\u4E0B\u6587\u6062\u590D\u65F6\u523B\u3002
5325
+ You are at or beyond the configured max context threshold. This is an emergency context-recovery moment.
5062
5326
 
5063
- \u4F60\u5FC5\u987B\u7ACB\u5373\u4F7F\u7528 \`compress\` \u5DE5\u5177\u3002\u5728\u5B8C\u6210\u538B\u7F29\u4E4B\u524D\uFF0C\u4E0D\u8981\u7EE7\u7EED\u6B63\u5E38\u63A2\u7D22\u3002
5327
+ You MUST call \`compress\` now. Do not continue normal exploration until compression is done.
5064
5328
 
5065
- \u5982\u679C\u4F60\u6B63\u5728\u8FDB\u884C\u5173\u952E\u7684\u539F\u5B50\u64CD\u4F5C\uFF0C\u5148\u5B8C\u6210\u8BE5\u539F\u5B50\u6B65\u9AA4\uFF0C\u7136\u540E\u7ACB\u5373\u538B\u7F29\u3002
5329
+ If mid-atomic-step, finish that one step, then compress immediately.
5066
5330
 
5067
- \u9009\u62E9\u8FC7\u7A0B
5068
- \u4ECE\u8F83\u65E9\u7684\u3001\u5DF2\u5B8C\u6210\u7684\u5386\u53F2\u5F00\u59CB\uFF0C\u5728\u4E00\u6B21\u64CD\u4F5C\u4E2D\u6355\u83B7\u5C3D\u53EF\u80FD\u591A\u7684\u8FC7\u65F6\u4E0A\u4E0B\u6587\u3002
5069
- \u9664\u975E\u6700\u65B0\u7684\u5DE5\u4F5C\u6D88\u606F\u5DF2\u660E\u663E\u7ED3\u675F\uFF0C\u5426\u5219\u4E0D\u8981\u9009\u62E9\u5B83\u4EEC\u3002
5331
+ Selection
5332
+ Start from older, already-finished history and capture as much stale context as possible in one pass.
5333
+ Do not select the latest working messages unless they are clearly concluded.
5070
5334
 
5071
- \u6458\u8981\u8981\u6C42
5072
- \u4F60\u7684\u6458\u8981\u5FC5\u987B\u8986\u76D6\u6240\u9009\u6D88\u606F\u4E2D\u7684\u6240\u6709\u5173\u952E\u7EC6\u8282\uFF0C\u4EE5\u786E\u4FDD\u5DE5\u4F5C\u53EF\u4EE5\u7EE7\u7EED\u3002
5073
- \u5982\u679C\u538B\u7F29\u8303\u56F4\u5305\u542B\u7528\u6237\u6D88\u606F\uFF0C\u5B8C\u6574\u4FDD\u7559\u7528\u6237\u610F\u56FE\u3002\u5BF9\u4E8E\u8F83\u77ED\u7684\u7528\u6237\u6D88\u606F\uFF0C\u4F18\u5148\u4F7F\u7528\u76F4\u63A5\u5F15\u7528\u4EE5\u907F\u514D\u8BED\u4E49\u6F02\u79FB\u3002
5335
+ Summary requirements
5336
+ Your summary must cover all key details in the selected messages so work can continue.
5337
+ If the range contains user messages, preserve their intent fully. For short user messages, prefer direct quotes to avoid semantic drift.
5074
5338
  </dcp-system-reminder>
5075
5339
  `;
5076
5340
 
5077
5341
  // lib/prompts/turn-nudge.ts
5078
5342
  var TURN_NUDGE = `
5079
- \u4E0A\u4E0B\u6587\u4F7F\u7528\u91CF\u5904\u4E8E\u4E2D\u7B49\u6C34\u5E73\u3002\u8003\u8651\u538B\u7F29\u5DF2\u5B8C\u6210\u7684\u5BF9\u8BDD\u8F6E\u6B21\u4EE5\u91CA\u653E\u7A7A\u95F4\u3002
5343
+ New user turn. This is the moment topic changes are most likely.
5344
+
5345
+ Is this turn's request a different topic/task from the previous one?
5346
+ - New or independent topic: call \`compress\` now to fold the previous topic's finished context into a summary. Its details no longer need verbatim retention.
5347
+ - Continuation of the same topic: you may keep it, but still watch for stale accumulation.
5348
+
5349
+ Topic change is the clearest, most objective compression trigger \u2014 do not miss it.
5080
5350
  `;
5081
5351
 
5082
5352
  // lib/prompts/iteration-nudge.ts
5083
5353
  var ITERATION_NUDGE = `
5084
- \u5DF2\u8FDB\u884C\u591A\u6B21\u8FED\u4EE3\u4F46\u672A\u6536\u5230\u65B0\u7684\u7528\u6237\u8F93\u5165\u3002\u8003\u8651\u538B\u7F29\u65E9\u671F\u6B65\u9AA4\u4EE5\u4FDD\u6301\u4E0A\u4E0B\u6587\u6E05\u6670\u3002
5354
+ Many iterations elapsed without new user input.
5355
+
5356
+ Has the current iteration's task effectively finished, or has its topic drifted away from earlier steps?
5357
+ - If earlier steps belong to a resolved problem or a superseded exploration, call \`compress\` now to fold them into a summary.
5358
+ - If the iteration still continues the same unfinished goal and early context is still needed, keep it.
5359
+
5360
+ Do not let finished exploration directions keep occupying context. Topic drift is the best moment to compress.
5085
5361
  `;
5086
5362
 
5087
5363
  // lib/prompts/extensions/system.ts