@lexwdex-org/opencode-dcp 3.3.4 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js 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,18 @@ 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
+ \`\`\`${externalHint}`;
1876
+ }
1877
+ function buildMessageFormatExtension(externalModelEnabled) {
1878
+ 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`;
1879
+ const externalHint = externalModelEnabled ? `
1880
+
1881
+ \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` : "";
1882
+ return `
1812
1883
  \u538B\u7F29\u683C\u5F0F
1813
1884
 
1814
1885
  \`\`\`
@@ -1818,11 +1889,14 @@ var MESSAGE_FORMAT_EXTENSION = `
1818
1889
  {
1819
1890
  messageId: string, // \u4EC5\u539F\u59CB\u6D88\u606F ID\uFF1AmNNNN\uFF08\u5FFD\u7565 priority \u7B49\u5143\u6570\u636E\u5C5E\u6027\uFF09
1820
1891
  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
1892
+ ${summaryField}
1822
1893
  }
1823
1894
  ]
1824
1895
  }
1825
- \`\`\``;
1896
+ \`\`\`${externalHint}`;
1897
+ }
1898
+ var RANGE_FORMAT_EXTENSION = buildRangeFormatExtension(false);
1899
+ var MESSAGE_FORMAT_EXTENSION = buildMessageFormatExtension(false);
1826
1900
 
1827
1901
  // lib/message-ids.ts
1828
1902
  var MESSAGE_REF_REGEX = /^m(\d{4})$/;
@@ -1985,11 +2059,17 @@ function resolveBoundaryIds(context, state, startId, endId) {
1985
2059
  const issues = [];
1986
2060
  const parsedStartId = parseBoundaryId(startId);
1987
2061
  const parsedEndId = parseBoundaryId(endId);
2062
+ const hint = formatAvailableBoundaryHint(lookup);
2063
+ const guidance = formatBoundaryUsageGuidance();
1988
2064
  if (parsedStartId === null) {
1989
- issues.push("startId is invalid. Use an injected message ID (mNNNN) or block ID (bN).");
2065
+ issues.push(
2066
+ `startId is invalid. Use an injected message ID (mNNNN) or block ID (bN).${hint}${guidance}`
2067
+ );
1990
2068
  }
1991
2069
  if (parsedEndId === null) {
1992
- issues.push("endId is invalid. Use an injected message ID (mNNNN) or block ID (bN).");
2070
+ issues.push(
2071
+ `endId is invalid. Use an injected message ID (mNNNN) or block ID (bN).${hint}${guidance}`
2072
+ );
1993
2073
  }
1994
2074
  if (issues.length > 0) {
1995
2075
  throw new Error(
@@ -2002,28 +2082,18 @@ function resolveBoundaryIds(context, state, startId, endId) {
2002
2082
  const startReference = lookup.get(parsedStartId.ref);
2003
2083
  const endReference = lookup.get(parsedEndId.ref);
2004
2084
  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
2085
  if (!startReference) {
2016
2086
  const wasKnown = state.messageIds.byRef.has(parsedStartId.ref);
2017
2087
  const reason = wasKnown ? " (message was likely compacted out of the conversation)" : " (invalid ID)";
2018
2088
  issues.push(
2019
- `startId ${parsedStartId.ref} is not available${reason}. Choose an injected ID visible in context.${hint}`
2089
+ `startId ${parsedStartId.ref} is not available${reason}. Choose an injected ID visible in context.${hint}${guidance}`
2020
2090
  );
2021
2091
  }
2022
2092
  if (!endReference) {
2023
2093
  const wasKnown = state.messageIds.byRef.has(parsedEndId.ref);
2024
2094
  const reason = wasKnown ? " (message was likely compacted out of the conversation)" : " (invalid ID)";
2025
2095
  issues.push(
2026
- `endId ${parsedEndId.ref} is not available${reason}. Choose an injected ID visible in context.${hint}`
2096
+ `endId ${parsedEndId.ref} is not available${reason}. Choose an injected ID visible in context.${hint}${guidance}`
2027
2097
  );
2028
2098
  }
2029
2099
  }
@@ -2129,6 +2199,17 @@ function resolveAnchorMessageId(startReference) {
2129
2199
  }
2130
2200
  return startReference.messageId;
2131
2201
  }
2202
+ function formatAvailableBoundaryHint(lookup) {
2203
+ const allKeys = Array.from(lookup.keys());
2204
+ const availableBlockRefs = allKeys.filter((key) => key.startsWith("b")).sort((a, b) => Number.parseInt(a.slice(1), 10) - Number.parseInt(b.slice(1), 10));
2205
+ const availableMsgRefs = allKeys.filter((key) => key.startsWith("m")).sort((a, b) => Number.parseInt(a.slice(1), 10) - Number.parseInt(b.slice(1), 10));
2206
+ const blockHint = availableBlockRefs.length ? ` Available block IDs: ${availableBlockRefs.join(", ")}.` : " No block IDs available.";
2207
+ 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).`;
2208
+ return `${msgHint}${blockHint}`;
2209
+ }
2210
+ function formatBoundaryUsageGuidance() {
2211
+ return " Use exactly one of the listed injected IDs. For already compressed content, use its bN block ID.";
2212
+ }
2132
2213
  function buildBoundaryLookup(context, state) {
2133
2214
  const lookup = /* @__PURE__ */ new Map();
2134
2215
  for (const [messageRef, messageId] of state.messageIds.byRef) {
@@ -2446,8 +2527,10 @@ function validateArgs(args) {
2446
2527
  if (typeof entry?.topic !== "string" || entry.topic.trim().length === 0) {
2447
2528
  throw new Error(`${prefix}.topic is required and must be a non-empty string`);
2448
2529
  }
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`);
2530
+ if (entry?.summary !== void 0) {
2531
+ if (typeof entry.summary !== "string" || entry.summary.trim().length === 0) {
2532
+ throw new Error(`${prefix}.summary must be a non-empty string when provided`);
2533
+ }
2451
2534
  }
2452
2535
  }
2453
2536
  }
@@ -4165,8 +4248,118 @@ ${output}`);
4165
4248
  return summary + heading + protectedOutputs.join("");
4166
4249
  }
4167
4250
 
4251
+ // lib/compress/external-inference.ts
4252
+ var ExternalModelError = class extends Error {
4253
+ constructor(message, kind, cause) {
4254
+ super(message);
4255
+ this.kind = kind;
4256
+ this.cause = cause;
4257
+ this.name = "ExternalModelError";
4258
+ }
4259
+ };
4260
+ function resolveCompressBaseUrl(url) {
4261
+ const normalized = url.replace(/\/$/, "");
4262
+ if (normalized.endsWith("/chat/completions")) {
4263
+ return normalized;
4264
+ }
4265
+ return `${normalized}/chat/completions`;
4266
+ }
4267
+ async function callExternalModel(endpoint, cfg, req) {
4268
+ const controller = new AbortController();
4269
+ const timeoutMs = cfg.timeout ?? 12e4;
4270
+ const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
4271
+ let response;
4272
+ try {
4273
+ response = await fetch(endpoint, {
4274
+ method: "POST",
4275
+ headers: {
4276
+ "Content-Type": "application/json",
4277
+ ...cfg.apiKey ? { Authorization: `Bearer ${cfg.apiKey}` } : {}
4278
+ },
4279
+ body: JSON.stringify({
4280
+ model: cfg.model,
4281
+ messages: [
4282
+ { role: "system", content: req.systemPrompt },
4283
+ { role: "user", content: req.userContent }
4284
+ ],
4285
+ temperature: 0
4286
+ }),
4287
+ signal: controller.signal
4288
+ });
4289
+ } catch (error) {
4290
+ if (error instanceof Error && error.name === "AbortError") {
4291
+ throw new ExternalModelError(
4292
+ `External model request timed out after ${timeoutMs}ms`,
4293
+ "timeout",
4294
+ error
4295
+ );
4296
+ }
4297
+ throw new ExternalModelError(
4298
+ `External model request failed: ${error instanceof Error ? error.message : String(error)}`,
4299
+ "network",
4300
+ error
4301
+ );
4302
+ } finally {
4303
+ clearTimeout(timeoutHandle);
4304
+ }
4305
+ if (!response.ok) {
4306
+ let errorBody = "";
4307
+ try {
4308
+ errorBody = await response.text();
4309
+ } catch {
4310
+ }
4311
+ throw new ExternalModelError(
4312
+ `External model returned HTTP ${response.status}: ${errorBody.slice(0, 200)}`,
4313
+ "http",
4314
+ { status: response.status, body: errorBody }
4315
+ );
4316
+ }
4317
+ let parsed;
4318
+ try {
4319
+ parsed = await response.json();
4320
+ } catch (error) {
4321
+ throw new ExternalModelError("External model returned invalid JSON", "parse", error);
4322
+ }
4323
+ const content = parsed?.choices?.[0]?.message?.content;
4324
+ if (typeof content !== "string" || content.trim().length === 0) {
4325
+ throw new ExternalModelError(
4326
+ "External model returned empty or missing summary content",
4327
+ "empty",
4328
+ parsed
4329
+ );
4330
+ }
4331
+ return content;
4332
+ }
4333
+ async function generateSummaryViaExternal(cfg, req) {
4334
+ const endpoint = resolveCompressBaseUrl(cfg.url);
4335
+ const maxRetries = cfg.retries ?? 1;
4336
+ let lastError;
4337
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
4338
+ try {
4339
+ return await callExternalModel(endpoint, cfg, req);
4340
+ } catch (error) {
4341
+ lastError = error;
4342
+ }
4343
+ }
4344
+ throw lastError;
4345
+ }
4346
+
4168
4347
  // lib/compress/message.ts
4169
- function buildSchema() {
4348
+ function extractMessageText(messageId, searchContext) {
4349
+ const msg = searchContext.rawMessagesById.get(messageId);
4350
+ if (!msg) return "";
4351
+ const parts = [];
4352
+ for (const part of msg.parts) {
4353
+ if (part.type === "text" && typeof part.text === "string") {
4354
+ parts.push(part.text);
4355
+ }
4356
+ }
4357
+ return parts.join("\n\n");
4358
+ }
4359
+ function buildSchema(externalModelEnabled) {
4360
+ const summaryField = externalModelEnabled ? tool.schema.string().optional().describe(
4361
+ "Complete technical summary replacing that one message (optional when external model is configured)"
4362
+ ) : tool.schema.string().describe("Complete technical summary replacing that one message");
4170
4363
  return {
4171
4364
  topic: tool.schema.string().describe(
4172
4365
  "Short label (3-5 words) for the overall batch - e.g., 'Closed Research Notes'"
@@ -4175,7 +4368,7 @@ function buildSchema() {
4175
4368
  tool.schema.object({
4176
4369
  messageId: tool.schema.string().describe("Raw message ID to compress (e.g. m0001)"),
4177
4370
  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")
4371
+ summary: summaryField
4179
4372
  })
4180
4373
  ).describe("Batch of individual message summaries to create in one tool call")
4181
4374
  };
@@ -4183,9 +4376,10 @@ function buildSchema() {
4183
4376
  function createCompressMessageTool(ctx) {
4184
4377
  ctx.prompts.reload();
4185
4378
  const runtimePrompts = ctx.prompts.getRuntimePrompts();
4379
+ const externalModelEnabled = ctx.config.compress.externalModel !== void 0;
4186
4380
  return tool({
4187
- description: runtimePrompts.compressMessage + MESSAGE_FORMAT_EXTENSION,
4188
- args: buildSchema(),
4381
+ description: runtimePrompts.compressMessage + buildMessageFormatExtension(externalModelEnabled),
4382
+ args: buildSchema(externalModelEnabled),
4189
4383
  async execute(args, toolCtx) {
4190
4384
  const input = args;
4191
4385
  validateArgs(input);
@@ -4204,9 +4398,29 @@ function createCompressMessageTool(ctx) {
4204
4398
  if (plans.length === 0 && skippedCount > 0) {
4205
4399
  throw new Error(formatIssues(skippedIssues, skippedCount));
4206
4400
  }
4401
+ if (ctx.config.compress.externalModel) {
4402
+ for (const plan of plans) {
4403
+ if (plan.entry.summary === void 0) {
4404
+ const userContent = extractMessageText(plan.entry.messageId, searchContext);
4405
+ const generated = await generateSummaryViaExternal(
4406
+ ctx.config.compress.externalModel,
4407
+ {
4408
+ systemPrompt: runtimePrompts.compressMessage,
4409
+ userContent
4410
+ }
4411
+ );
4412
+ plan.entry.summary = generated;
4413
+ }
4414
+ }
4415
+ }
4207
4416
  const notifications = [];
4208
4417
  const preparedPlans = [];
4209
4418
  for (const plan of plans) {
4419
+ if (plan.entry.summary === void 0) {
4420
+ throw new Error(
4421
+ "\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"
4422
+ );
4423
+ }
4210
4424
  const summaryWithPromptInfo = appendProtectedPromptInfo(
4211
4425
  plan.entry.summary,
4212
4426
  plan.selection,
@@ -4287,8 +4501,10 @@ function validateArgs2(args) {
4287
4501
  if (typeof entry?.endId !== "string" || entry.endId.trim().length === 0) {
4288
4502
  throw new Error(`${prefix}.endId is required and must be a non-empty string`);
4289
4503
  }
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`);
4504
+ if (entry?.summary !== void 0) {
4505
+ if (typeof entry.summary !== "string" || entry.summary.trim().length === 0) {
4506
+ throw new Error(`${prefix}.summary must be a non-empty string when provided`);
4507
+ }
4292
4508
  }
4293
4509
  }
4294
4510
  }
@@ -4494,7 +4710,23 @@ ${right}`;
4494
4710
  }
4495
4711
 
4496
4712
  // lib/compress/range.ts
4497
- function buildSchema2() {
4713
+ function extractRangeText(selection, searchContext) {
4714
+ const parts = [];
4715
+ for (const messageId of selection.messageIds) {
4716
+ const msg = searchContext.rawMessagesById.get(messageId);
4717
+ if (!msg) continue;
4718
+ for (const part of msg.parts) {
4719
+ if (part.type === "text" && typeof part.text === "string") {
4720
+ parts.push(part.text);
4721
+ }
4722
+ }
4723
+ }
4724
+ return parts.join("\n\n");
4725
+ }
4726
+ function buildSchema2(externalModelEnabled) {
4727
+ const summaryField = externalModelEnabled ? tool2.schema.string().optional().describe(
4728
+ "Complete technical summary replacing all content in range (optional when external model is configured)"
4729
+ ) : tool2.schema.string().describe("Complete technical summary replacing all content in range");
4498
4730
  return {
4499
4731
  topic: tool2.schema.string().describe("Short label (3-5 words) for display - e.g., 'Auth System Exploration'"),
4500
4732
  content: tool2.schema.array(
@@ -4503,7 +4735,7 @@ function buildSchema2() {
4503
4735
  "Message or block ID marking the beginning of range (e.g. m0001, b2)"
4504
4736
  ),
4505
4737
  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")
4738
+ summary: summaryField
4507
4739
  })
4508
4740
  ).describe(
4509
4741
  "One or more ranges to compress, each with start/end boundaries and a summary"
@@ -4513,9 +4745,10 @@ function buildSchema2() {
4513
4745
  function createCompressRangeTool(ctx) {
4514
4746
  ctx.prompts.reload();
4515
4747
  const runtimePrompts = ctx.prompts.getRuntimePrompts();
4748
+ const externalModelEnabled = ctx.config.compress.externalModel !== void 0;
4516
4749
  return tool2({
4517
- description: runtimePrompts.compressRange + RANGE_FORMAT_EXTENSION,
4518
- args: buildSchema2(),
4750
+ description: runtimePrompts.compressRange + buildRangeFormatExtension(externalModelEnabled),
4751
+ args: buildSchema2(externalModelEnabled),
4519
4752
  async execute(args, toolCtx) {
4520
4753
  const input = args;
4521
4754
  validateArgs2(input);
@@ -4527,10 +4760,30 @@ function createCompressRangeTool(ctx) {
4527
4760
  );
4528
4761
  const resolvedPlans = resolveRanges(input, searchContext, ctx.state);
4529
4762
  validateNonOverlapping(resolvedPlans);
4763
+ if (ctx.config.compress.externalModel) {
4764
+ for (const plan of resolvedPlans) {
4765
+ if (plan.entry.summary === void 0) {
4766
+ const userContent = extractRangeText(plan.selection, searchContext);
4767
+ const generated = await generateSummaryViaExternal(
4768
+ ctx.config.compress.externalModel,
4769
+ {
4770
+ systemPrompt: runtimePrompts.compressRange,
4771
+ userContent
4772
+ }
4773
+ );
4774
+ plan.entry.summary = generated;
4775
+ }
4776
+ }
4777
+ }
4530
4778
  const notifications = [];
4531
4779
  const preparedPlans = [];
4532
4780
  let totalCompressedMessages = 0;
4533
4781
  for (const plan of resolvedPlans) {
4782
+ if (plan.entry.summary === void 0) {
4783
+ throw new Error(
4784
+ "\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"
4785
+ );
4786
+ }
4534
4787
  const parsedPlaceholders = parseBlockPlaceholders(plan.entry.summary);
4535
4788
  const missingBlockIds = validateSummaryPlaceholders(
4536
4789
  parsedPlaceholders,