acp-kernel 0.0.7 → 0.0.8

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.
@@ -1 +1 @@
1
- {"version":3,"file":"compress.d.ts","sourceRoot":"","sources":["../src/compress.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAcpB,OAAO,EAGL,KAAK,YAAY,EAElB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EACV,sBAAsB,EACtB,gBAAgB,EAChB,gBAAgB,EAEhB,MAAM,EAEN,WAAW,EAGX,iBAAiB,EAEjB,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,KAAK;IACpB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACxC;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,iBAAiB,CAAC;IACxD,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,sBAAsB,CAAC;IACvE,YAAY,IAAI,YAAY,EAAE,CAAC;IAC/B,UAAU,CACR,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,gBAAgB,GACtB,gBAAgB,GAAG,SAAS,CAAC;IAChC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,GAAG,gBAAgB,EAAE,CAAC;IACnE,MAAM,CACJ,KAAK,EAAE,gBAAgB,EACvB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,GACb,YAAY,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,KAAK,EAAE,gBAAgB,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE;QACN,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,eAAe,CAAC,EAAE,MAAM,CAAC;KAC1B,EAAE,CAAC;IACJ,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,KAAK,EAAE,gBAAgB,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACnC;AAED,wBAAgB,UAAU,CAAC,KAAK,GAAE,KAAU,GAAG,eAAe,CAyM7D;AA0kBD,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
1
+ {"version":3,"file":"compress.d.ts","sourceRoot":"","sources":["../src/compress.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAcpB,OAAO,EAGL,KAAK,YAAY,EAElB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EACV,sBAAsB,EACtB,gBAAgB,EAChB,gBAAgB,EAEhB,MAAM,EAEN,WAAW,EAGX,iBAAiB,EAEjB,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,KAAK;IACpB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACxC;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,iBAAiB,CAAC;IACxD,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,sBAAsB,CAAC;IACvE,YAAY,IAAI,YAAY,EAAE,CAAC;IAC/B,UAAU,CACR,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,gBAAgB,GACtB,gBAAgB,GAAG,SAAS,CAAC;IAChC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,GAAG,gBAAgB,EAAE,CAAC;IACnE,MAAM,CACJ,KAAK,EAAE,gBAAgB,EACvB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,GACb,YAAY,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,KAAK,EAAE,gBAAgB,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE;QACN,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,eAAe,CAAC,EAAE,MAAM,CAAC;KAC1B,EAAE,CAAC;IACJ,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,KAAK,EAAE,gBAAgB,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACnC;AAED,wBAAgB,UAAU,CAAC,KAAK,GAAE,KAAU,GAAG,eAAe,CAgN7D;AA8lBD,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
package/dist/index.js CHANGED
@@ -269,9 +269,9 @@ import { createRequire } from "module";
269
269
  var require2 = createRequire(import.meta.url);
270
270
  function defaultCountTokens(text) {
271
271
  if (!text) return 0;
272
- const ascii = text.match(/[a-zA-Z][a-zA-Z0-9_'-]*/g);
273
272
  const cjk = text.match(/[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/g);
274
- return (ascii?.length ?? 0) + (cjk?.length ?? 0);
273
+ const cjkCount = cjk?.length ?? 0;
274
+ return cjkCount + Math.ceil((text.length - cjkCount) / 4);
275
275
  }
276
276
  function estimateTokensFast(text) {
277
277
  if (!text) return 0;
@@ -704,6 +704,7 @@ var renderRefsNode = {
704
704
  };
705
705
 
706
706
  // src/protected.ts
707
+ var ALWAYS_PROTECTED_TOOLS = ["compress"];
707
708
  function matchToolPattern(toolName, pattern) {
708
709
  if (pattern.endsWith("*")) {
709
710
  return toolName.startsWith(pattern.slice(0, -1));
@@ -711,13 +712,34 @@ function matchToolPattern(toolName, pattern) {
711
712
  return toolName === pattern;
712
713
  }
713
714
  function isMessageProtected(msg, config) {
714
- if (msg.contentType !== "tool-call" || !msg.toolName) return false;
715
+ if (msg.contentType !== "tool-call" && msg.contentType !== "tool-result" || !msg.toolName) {
716
+ return false;
717
+ }
718
+ if (ALWAYS_PROTECTED_TOOLS.includes(msg.toolName)) {
719
+ return true;
720
+ }
715
721
  for (const pattern of config.protectedTools) {
716
722
  if (matchToolPattern(msg.toolName, pattern)) return true;
717
723
  }
718
724
  if (config.isToolProtected?.(msg.toolName, msg.text)) return true;
719
725
  return false;
720
726
  }
727
+ function collectProtectedToolCallIds(messages, config) {
728
+ const ids = /* @__PURE__ */ new Set();
729
+ for (const m of messages) {
730
+ if (m.contentType === "tool-call" && m.toolCallId && isMessageProtected(m, config)) {
731
+ ids.add(m.toolCallId);
732
+ }
733
+ }
734
+ return ids;
735
+ }
736
+ function isMessageProtectedWithPairing(msg, config, protectedCallIds) {
737
+ if (isMessageProtected(msg, config)) return true;
738
+ if (msg.contentType === "tool-result" && msg.toolCallId && protectedCallIds.has(msg.toolCallId)) {
739
+ return true;
740
+ }
741
+ return false;
742
+ }
721
743
 
722
744
  // src/tool-pairs.ts
723
745
  function adjustBoundariesForToolPairs(startIndex, endIndex, messages, maxScan = 20) {
@@ -795,24 +817,27 @@ function computeProtectedRefs(messages, state, config) {
795
817
  tokenAccum += visible[i].tokens;
796
818
  }
797
819
  }
798
- for (let i = messages.length - 1; i >= 0; i--) {
799
- const msg = messages[i];
800
- if (msg.role !== "user" || isSyntheticOrPruned(msg, state)) continue;
801
- const ref = state.messageRefs.byRaw[msg.id];
802
- if (ref && ref !== "BLOCKED") result.add(ref);
803
- break;
820
+ if (preserveN > 0) {
821
+ for (let i = messages.length - 1; i >= 0; i--) {
822
+ const msg = messages[i];
823
+ if (msg.role !== "user" || isSyntheticOrPruned(msg, state)) continue;
824
+ const ref = state.messageRefs.byRaw[msg.id];
825
+ if (ref && ref !== "BLOCKED") result.add(ref);
826
+ break;
827
+ }
804
828
  }
805
829
  return result;
806
830
  }
807
831
  function buildCompressibleRanges(messages, state, config, protectedZoneRefs) {
808
832
  const compressibleMsgs = [];
809
833
  const protectedMsgs = [];
834
+ const protectedCallIds = collectProtectedToolCallIds(messages, config);
810
835
  for (const msg of messages) {
811
836
  if (isSyntheticOrPruned(msg, state)) continue;
812
837
  const ref = state.messageRefs.byRaw[msg.id];
813
838
  if (!ref || ref === "BLOCKED") continue;
814
839
  const rn = refNum(ref);
815
- if (isMessageProtected(msg, config)) {
840
+ if (isMessageProtectedWithPairing(msg, config, protectedCallIds)) {
816
841
  protectedMsgs.push({
817
842
  ref,
818
843
  refNum: rn,
@@ -828,7 +853,8 @@ function buildCompressibleRanges(messages, state, config, protectedZoneRefs) {
828
853
  ref,
829
854
  refNum: rn,
830
855
  tokens: estimateMessageTokens(msg),
831
- isTool: isToolMessage(msg)
856
+ isTool: isToolMessage(msg),
857
+ isUser: msg.role === "user"
832
858
  });
833
859
  }
834
860
  const compressible = [];
@@ -836,7 +862,7 @@ function buildCompressibleRanges(messages, state, config, protectedZoneRefs) {
836
862
  let prevRefNum = -2;
837
863
  for (const info of compressibleMsgs) {
838
864
  const hasGap = info.refNum > prevRefNum + 1;
839
- if (cur && hasGap) {
865
+ if (cur && (info.isUser && cur.count >= 3 || hasGap)) {
840
866
  compressible.push(cur);
841
867
  cur = null;
842
868
  }
@@ -919,6 +945,7 @@ function createCore(ports = {}) {
919
945
  let blocksCreated = 0;
920
946
  let tokensCompressed = 0;
921
947
  const errors = [];
948
+ const protectedMessageIds = input.protectedMessageIds ?? computeProtectedRefs(input.messages, input.state, input.config);
922
949
  const preExistingCoverage = collectCoverage(state);
923
950
  const rangeIndexSets = [];
924
951
  for (const spec of input.ranges) {
@@ -1006,7 +1033,7 @@ function createCore(ports = {}) {
1006
1033
  state,
1007
1034
  runId,
1008
1035
  config: input.config,
1009
- protectedMessageIds: input.protectedMessageIds,
1036
+ protectedMessageIds,
1010
1037
  countTokens,
1011
1038
  preExistingCoverage
1012
1039
  });
@@ -1248,11 +1275,25 @@ function applySingleRange(input) {
1248
1275
  const directMessageIds = [...effectiveMessageIds].filter(
1249
1276
  (id) => !input.preExistingCoverage.has(id)
1250
1277
  );
1251
- const { filteredIds, appendedProtectedText } = filterProtectedToolMessages(
1278
+ const filteredIds = filterProtectedToolMessages(
1252
1279
  directMessageIds,
1253
1280
  input.messages,
1254
1281
  input.config
1255
1282
  );
1283
+ const protectedRefs = input.protectedMessageIds;
1284
+ const hitProtectedRaw = protectedRefs ? filteredIds.filter((id) => {
1285
+ const ref = input.state.messageRefs.byRaw[id];
1286
+ return ref !== void 0 && protectedRefs.has(ref);
1287
+ }) : [];
1288
+ if (hitProtectedRaw.length > 0) {
1289
+ const recentN = input.config.preserveRecentMessages;
1290
+ const hitRefs = hitProtectedRaw.map((id) => input.state.messageRefs.byRaw[id]).filter(Boolean);
1291
+ throw new Error(
1292
+ `Range includes protected messages (the last ${recentN} messages and/or the most recent user message) which must not be compressed: ${hitRefs.join(
1293
+ ", "
1294
+ )}. Adjust startId/endId to exclude them.`
1295
+ );
1296
+ }
1256
1297
  validateCompressionRange(input, filteredIds, consumedBlockIds.length);
1257
1298
  let compressedTokens = 0;
1258
1299
  for (const id of filteredIds) {
@@ -1266,22 +1307,12 @@ function applySingleRange(input) {
1266
1307
  }
1267
1308
  }
1268
1309
  const blockId = allocateBlockId(input.state);
1269
- const finalSummary = appendedProtectedText.length > 0 ? `${input.spec.summary}
1270
-
1271
- The following protected tool calls were used in this conversation as well:
1272
- ${appendedProtectedText.join("")}` : input.spec.summary;
1273
- const maxLen = input.spec.summaryMaxChars ?? input.config.compress.maxSummaryLength;
1274
- if (maxLen > 0 && finalSummary.length > maxLen) {
1275
- throw new Error(
1276
- `Summary too long after appending protected content (${finalSummary.length} chars, max ${maxLen}). Reduce the summary or remove protected tools from the range.`
1277
- );
1278
- }
1279
1310
  const block = {
1280
1311
  blockId,
1281
1312
  runId: input.runId,
1282
1313
  tier: outputTier,
1283
1314
  topic: input.spec.topic,
1284
- summary: finalSummary,
1315
+ summary: input.spec.summary,
1285
1316
  directMessageIds: filteredIds,
1286
1317
  effectiveMessageIds: [...effectiveMessageIds],
1287
1318
  directBlockIds: [...consumedBlockIds],
@@ -1344,41 +1375,30 @@ function validateCompressionRange(input, directMessageIds, consumedBlockCount) {
1344
1375
  }
1345
1376
  }
1346
1377
  function filterProtectedToolMessages(directMessageIds, messages, config) {
1347
- const removedToolCallIds = /* @__PURE__ */ new Set();
1348
- const allProtectedCallIds = /* @__PURE__ */ new Set();
1378
+ const protectedCallIds = /* @__PURE__ */ new Set();
1349
1379
  const removedIds = /* @__PURE__ */ new Set();
1350
- const appendedProtectedText = [];
1351
1380
  for (const msg of messages) {
1352
1381
  if (isMessageProtected(msg, config) && msg.toolCallId) {
1353
- allProtectedCallIds.add(msg.toolCallId);
1382
+ protectedCallIds.add(msg.toolCallId);
1354
1383
  }
1355
1384
  }
1356
1385
  for (const id of directMessageIds) {
1357
1386
  const msg = messages.find((m) => m.id === id);
1358
1387
  if (!msg) continue;
1359
- if (!isMessageProtected(msg, config)) continue;
1360
- removedIds.add(id);
1361
- if (msg.toolCallId) removedToolCallIds.add(msg.toolCallId);
1362
- if (msg.text) {
1363
- appendedProtectedText.push(
1364
- `
1365
- ### Protected: ${msg.toolName ?? "tool"}
1366
- ${msg.text}`
1367
- );
1388
+ if (isMessageProtected(msg, config)) {
1389
+ removedIds.add(id);
1390
+ if (msg.toolCallId) protectedCallIds.add(msg.toolCallId);
1368
1391
  }
1369
1392
  }
1370
1393
  for (const id of directMessageIds) {
1371
1394
  if (removedIds.has(id)) continue;
1372
1395
  const msg = messages.find((m) => m.id === id);
1373
1396
  if (!msg) continue;
1374
- if (msg.contentType === "tool-result" && msg.toolCallId && (removedToolCallIds.has(msg.toolCallId) || allProtectedCallIds.has(msg.toolCallId))) {
1397
+ if (msg.contentType === "tool-result" && msg.toolCallId && protectedCallIds.has(msg.toolCallId)) {
1375
1398
  removedIds.add(id);
1376
1399
  }
1377
1400
  }
1378
- return {
1379
- filteredIds: directMessageIds.filter((id) => !removedIds.has(id)),
1380
- appendedProtectedText
1381
- };
1401
+ return directMessageIds.filter((id) => !removedIds.has(id));
1382
1402
  }
1383
1403
  function resolveTargetTier(state, nestedBlockIds, isBlockBoundary) {
1384
1404
  if (!isBlockBoundary) return 1;
@@ -1429,23 +1449,30 @@ function decideNudge(input) {
1429
1449
  shouldContextNudge = growthSinceReference >= growthFloor;
1430
1450
  }
1431
1451
  let tier = null;
1452
+ let tierTargetBlocks = [];
1432
1453
  if (config.tiers.enabled) {
1433
1454
  const active = activeBlocks(state);
1434
1455
  const activeT1 = active.filter((b) => b.tier === 1);
1435
1456
  const activeT2 = active.filter((b) => b.tier === 2);
1436
- if (activeT2.length >= config.tiers.tier3Trigger) tier = 3;
1437
- else if (activeT1.length >= config.tiers.tier2Trigger) tier = 2;
1457
+ if (activeT2.length >= config.tiers.tier3Trigger) {
1458
+ tier = 3;
1459
+ tierTargetBlocks = activeT2;
1460
+ } else if (activeT1.length >= config.tiers.tier2Trigger) {
1461
+ tier = 2;
1462
+ tierTargetBlocks = activeT1;
1463
+ }
1438
1464
  }
1439
1465
  const rec = recommendation;
1440
1466
  const nothingToCompress = rec ? rec.contextRanges.compressible.length === 0 : false;
1441
- const shouldInject = shouldContextNudge || tier !== null;
1467
+ const shouldInject = shouldContextNudge;
1442
1468
  let reason;
1443
1469
  if (!shouldInject) {
1444
- reason = `growth ${growthSinceReference} < threshold ${effectiveThreshold}${hasPendingNudge ? " (halved)" : ""} (floor ${growthFloor})`;
1470
+ const tierHint = tier !== null ? `, tier-${tier} ready (distill when growth reaches threshold)` : "";
1471
+ reason = `growth ${growthSinceReference} < threshold ${effectiveThreshold}${hasPendingNudge ? " (halved)" : ""} (floor ${growthFloor})${tierHint}`;
1445
1472
  } else if (emergencyOverride) {
1446
1473
  reason = `EMERGENCY: usage ${Math.round(usage * 100)}% >= ${Math.round(config.nudge.emergencyThresholdPct * 100)}%`;
1447
1474
  } else if (tier !== null) {
1448
- reason = `tier-${tier} distillation trigger`;
1475
+ reason = `growth ${growthSinceReference} >= ${effectiveThreshold}${hasPendingNudge ? " (halved)" : ""}, usage ${Math.round(usage * 100)}%, tier-${tier} distillation`;
1449
1476
  } else if (nothingToCompress) {
1450
1477
  reason = `growth ${growthSinceReference} >= ${effectiveThreshold} but no specific ranges worth compressing`;
1451
1478
  } else {
@@ -1457,6 +1484,7 @@ function decideNudge(input) {
1457
1484
  reason,
1458
1485
  compressibleRanges: rec?.recommendedRanges ?? [],
1459
1486
  protectedRanges: rec?.contextRanges.protected ?? [],
1487
+ tierTargetBlocks,
1460
1488
  contextUsage: usage,
1461
1489
  tier,
1462
1490
  breakdown: {
@@ -1733,6 +1761,18 @@ function mergeRanges(compressible, protected_) {
1733
1761
  }
1734
1762
  return merged;
1735
1763
  }
1764
+ function formatTierTargetBlocks(blocks) {
1765
+ if (blocks.length === 0) {
1766
+ return "Target blocks: (none \u2014 no tier blocks found)";
1767
+ }
1768
+ const lines = blocks.map((b) => {
1769
+ const summaryTokens = Math.ceil((b.summary ?? "").length / 4);
1770
+ const topic = b.topic ? ` "${b.topic}"` : "";
1771
+ return ` ${b.blockId} ${b.effectiveMessageIds.length} msgs ${formatK(b.compressedTokens)}\u2192${formatK(summaryTokens)}${topic}`;
1772
+ });
1773
+ return `Target ${blocks[0].tier === 1 ? "tier-1" : "tier-2"} blocks to distill (${blocks.length}):
1774
+ ${lines.join("\n")}`;
1775
+ }
1736
1776
  function formatRanges(compressible, protected_) {
1737
1777
  const merged = mergeRanges(compressible, protected_);
1738
1778
  if (merged.length === 0) {
@@ -1756,6 +1796,10 @@ function renderNudgeText(decision) {
1756
1796
  const rangesStr = formatRanges(decision.compressibleRanges, decision.protectedRanges ?? []);
1757
1797
  if (decision.tier !== null && decision.tier >= 2) {
1758
1798
  const isT2 = decision.tier === 2;
1799
+ const targets = decision.tierTargetBlocks ?? [];
1800
+ const blockList = formatTierTargetBlocks(targets);
1801
+ const startId = targets[0]?.blockId ?? "b1";
1802
+ const endId = targets[targets.length - 1]?.blockId ?? "b5";
1759
1803
  return {
1760
1804
  voice: "gentle",
1761
1805
  text: [
@@ -1764,8 +1808,9 @@ function renderNudgeText(decision) {
1764
1808
  breakdownStr,
1765
1809
  "",
1766
1810
  `[TIER ${decision.tier} ${isT2 ? "DISTILLATION" : "CONDENSATION"} TRIGGER]`,
1767
- isT2 ? "Your tier-1 compression summaries have accumulated. Distill them into a single denser tier-2 summary. Use block IDs as boundaries." : "Your tier-2 compression summaries have accumulated. Condense them further into a tier-3 ultra-condensed summary. Use block IDs as boundaries.",
1768
- `Example: compress({ content: [{ startId: "b1", endId: "b5", summary: "..." }] })`,
1811
+ isT2 ? `Your tier-1 compression summaries have accumulated. Distill them into a single denser tier-2 summary. Use block IDs as boundaries.` : `Your tier-2 compression summaries have accumulated. Condense them further into a tier-3 ultra-condensed summary. Use block IDs as boundaries.`,
1812
+ blockList,
1813
+ `Example: compress({ content: [{ startId: "${startId}", endId: "${endId}", summary: "..." }] })`,
1769
1814
  "",
1770
1815
  HOW_TO_COMPRESS_RULES,
1771
1816
  "",