billion-context-dsh 0.1.9 → 0.2.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/README.en.md +19 -6
- package/README.md +18 -6
- package/cordis.patch.yml +11 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +303 -41
- package/dist/index.js.map +1 -1
- package/dist/nudge.d.ts +16 -5
- package/dist/prompts.d.ts +91 -0
- package/dist/system-prompt.d.ts +6 -2
- package/dist/tools.d.ts +3 -0
- package/package.json +19 -1
package/dist/index.js
CHANGED
|
@@ -582,9 +582,151 @@ function kernelConfigFor(input) {
|
|
|
582
582
|
|
|
583
583
|
// src/nudge.ts
|
|
584
584
|
import {
|
|
585
|
-
|
|
585
|
+
COMPRESS_PHILOSOPHY as COMPRESS_PHILOSOPHY2,
|
|
586
|
+
TIER2_DISTILL_RULES as TIER2_DISTILL_RULES2,
|
|
587
|
+
TIER3_CONDENSE_RULES as TIER3_CONDENSE_RULES2,
|
|
588
|
+
defaultCountTokens as defaultCountTokens2,
|
|
589
|
+
renderNudgeText
|
|
586
590
|
} from "acp-kernel";
|
|
587
591
|
import { createUserMessage as createUserMessage2 } from "@deepseek-ai/dsh-llm";
|
|
592
|
+
|
|
593
|
+
// src/prompts.ts
|
|
594
|
+
import { COMPRESS_PHILOSOPHY, HOW_TO_COMPRESS_RULES, TIER2_DISTILL_RULES, TIER3_CONDENSE_RULES } from "acp-kernel";
|
|
595
|
+
var NUDGE_ALLOWED = {
|
|
596
|
+
normal: /* @__PURE__ */ new Set(["pct", "philosophy"]),
|
|
597
|
+
emergency: /* @__PURE__ */ new Set(["pct", "philosophy"]),
|
|
598
|
+
guidance: /* @__PURE__ */ new Set(),
|
|
599
|
+
tier: /* @__PURE__ */ new Set(["tier", "count", "prevTier", "tokens", "seqs"]),
|
|
600
|
+
breakdown: /* @__PURE__ */ new Set(["system", "tool", "summaries", "code", "text"]),
|
|
601
|
+
growth: /* @__PURE__ */ new Set(["growth"]),
|
|
602
|
+
tip: /* @__PURE__ */ new Set()
|
|
603
|
+
};
|
|
604
|
+
var RANGE_TABLE_ALLOWED = {
|
|
605
|
+
header: /* @__PURE__ */ new Set(["surface"]),
|
|
606
|
+
title: /* @__PURE__ */ new Set(["count"]),
|
|
607
|
+
line: /* @__PURE__ */ new Set(["start", "end", "count", "tokens"]),
|
|
608
|
+
footer: /* @__PURE__ */ new Set()
|
|
609
|
+
};
|
|
610
|
+
var TOOLS_ALLOWED = {
|
|
611
|
+
compress: /* @__PURE__ */ new Set(),
|
|
612
|
+
decompress: /* @__PURE__ */ new Set(),
|
|
613
|
+
searchContext: /* @__PURE__ */ new Set(),
|
|
614
|
+
acpStatus: /* @__PURE__ */ new Set()
|
|
615
|
+
};
|
|
616
|
+
var SYSTEM_ALLOWED = /* @__PURE__ */ new Set(["philosophy", "howToCompressRules", "tier2DistillRules", "tier3CondenseRules"]);
|
|
617
|
+
function validateTemplate(template, allowed, path) {
|
|
618
|
+
const re = /\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
619
|
+
let match;
|
|
620
|
+
while ((match = re.exec(template)) !== null) {
|
|
621
|
+
const name = match[1];
|
|
622
|
+
if (!allowed.has(name)) {
|
|
623
|
+
throw new Error(
|
|
624
|
+
`${path} contains unknown placeholder {${name}} \u2014 allowed: ${[...allowed].join(", ") || "(none)"}`
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return template;
|
|
629
|
+
}
|
|
630
|
+
function renderTemplate(template, vars) {
|
|
631
|
+
return template.replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, name) => {
|
|
632
|
+
const value = vars[name];
|
|
633
|
+
if (value === void 0) {
|
|
634
|
+
throw new Error(
|
|
635
|
+
`renderTemplate: missing value for placeholder {${name}} in template "${template.slice(0, 60)}\u2026"`
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
return String(value);
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
function mergeGroup(defaults, override, allowed, path) {
|
|
642
|
+
if (override == null) return defaults;
|
|
643
|
+
const out = {};
|
|
644
|
+
for (const key of Object.keys(defaults)) {
|
|
645
|
+
const value = override[key];
|
|
646
|
+
out[key] = value === null || value === void 0 ? defaults[key] : validateTemplate(value, allowed[key], `${path}.${String(key)}`);
|
|
647
|
+
}
|
|
648
|
+
return out;
|
|
649
|
+
}
|
|
650
|
+
function resolvePrompts(input) {
|
|
651
|
+
if (input === void 0) return DEFAULT_RESOLVED;
|
|
652
|
+
return {
|
|
653
|
+
nudge: mergeGroup(DEFAULT_PROMPTS.nudge, input.nudge, NUDGE_ALLOWED, "prompts.nudge"),
|
|
654
|
+
rangeTable: mergeGroup(DEFAULT_PROMPTS.rangeTable, input.rangeTable, RANGE_TABLE_ALLOWED, "prompts.rangeTable"),
|
|
655
|
+
tools: mergeGroup(DEFAULT_PROMPTS.tools, input.tools, TOOLS_ALLOWED, "prompts.tools"),
|
|
656
|
+
systemPromptTemplate: input.systemPrompt === null || input.systemPrompt === void 0 ? DEFAULT_PROMPTS.systemPromptTemplate : validateTemplate(input.systemPrompt, SYSTEM_ALLOWED, "prompts.systemPrompt")
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
function renderSystemPrompt(prompts) {
|
|
660
|
+
return renderTemplate(prompts.systemPromptTemplate, {
|
|
661
|
+
philosophy: COMPRESS_PHILOSOPHY,
|
|
662
|
+
howToCompressRules: HOW_TO_COMPRESS_RULES,
|
|
663
|
+
tier2DistillRules: TIER2_DISTILL_RULES,
|
|
664
|
+
tier3CondenseRules: TIER3_CONDENSE_RULES
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
var DEFAULT_PROMPTS = {
|
|
668
|
+
nudge: {
|
|
669
|
+
// 与 kernel nudge-text.ts EFFICIENCY_NOTE 逐字对齐——不含 "Context usage is at X%"
|
|
670
|
+
// 陈述(usage 只通过 breakdown 传达);{pct} 仍可用作自定义占位符。
|
|
671
|
+
normal: "This is an efficiency nudge to compress early and keep context lean \u2014 not an overflow warning. A separate, stronger alert will appear if the context is actually full.\n\n{philosophy}",
|
|
672
|
+
emergency: "\u26A0\uFE0F Context limit reached \u2014 compress now. Prioritize consumed tool outputs.\n\n{philosophy}",
|
|
673
|
+
guidance: HOW_TO_COMPRESS_RULES,
|
|
674
|
+
tier: "Tier {tier}: {count} tier-{prevTier} block(s) distillable ({tokens} tokens) \u2014 compress their summary node(s) [seqs {seqs}] to reclaim the original messages.",
|
|
675
|
+
breakdown: "Context breakdown: {system}K system | {tool}K tool | {summaries}K summaries | {code}K code | {text}K text",
|
|
676
|
+
growth: "+{growth}K since last nudge",
|
|
677
|
+
tip: "\u{1F4A1} Compress all ranges in one call (pass multiple content entries: `content: [{...}, {...}]`)."
|
|
678
|
+
},
|
|
679
|
+
rangeTable: {
|
|
680
|
+
header: "Surface: {surface}",
|
|
681
|
+
title: "Compressible ranges (suggestions only \u2014 compress any consumed span; refs are surface seqs):",
|
|
682
|
+
line: " - seq {start}..{end} \u2014 {count} messages, ~{tokens} tokens",
|
|
683
|
+
footer: "Compress with: compress({ content: [{ startSeq, endSeq, summary }] }) \u2014 content is an array: batch multiple unrelated segments in one call, each entry its own block. Keep ranges disjoint.\nSnapshot taken at nudge time: the seqs go stale once the surface moves (a later compress shadows them), so re-run acp_status for fresh refs before compressing."
|
|
684
|
+
},
|
|
685
|
+
tools: {
|
|
686
|
+
compress: "Replace older conversation ranges with dense summaries you write. Each message seq is a surface reference. Single range: compress({ content: [{ startSeq, endSeq, summary }] }). Batch multiple unrelated ranges in one call (each content entry becomes its own block); keep ranges disjoint. Never compress content the current step is actively using. Seq refs must come from the CURRENT surface (acp_status or the latest nudge): a span whose edges were shadowed by an earlier compress is auto-remapped to its still-live content, a fully compressed span is reported as already compressed, and invented/other-session seqs fail with guidance.",
|
|
687
|
+
decompress: "Recover the original content of a compressed block by its blockId (read-only; does not unshadow the range).",
|
|
688
|
+
searchContext: "Search inside compressed blocks (summaries and original content) for information the model no longer sees in context.",
|
|
689
|
+
acpStatus: "Report the ACP block ledger: compressed blocks, reclaimed tokens, and current context pressure."
|
|
690
|
+
},
|
|
691
|
+
systemPromptTemplate: `Active Context Pruning \u2014 model-driven context management
|
|
692
|
+
|
|
693
|
+
YOU decide whether and when to compress context. The nudge is an efficiency notification: when you see one, consider which ranges you have genuinely consumed and could summarise to keep working context lean.
|
|
694
|
+
|
|
695
|
+
{philosophy}
|
|
696
|
+
|
|
697
|
+
WHEN TO COMPRESS:
|
|
698
|
+
- A sub-agent or delegated task has returned a large result that you have already extracted the key facts from.
|
|
699
|
+
- Verbose command output (build/test logs, git diff, directory listings) where you have already used the information you need.
|
|
700
|
+
- Exploration that led nowhere.
|
|
701
|
+
- Repeated reads of the same file or repeated status checks once the decision is recorded.
|
|
702
|
+
- Resolved discussion threads where a decision has been captured in summary or in code.
|
|
703
|
+
- Intermediate steps of a completed multi-step task, once the final result is recorded.
|
|
704
|
+
- A task phase has ended \u2014 bug hunt complete, root cause found, exploration done, research sprint wrapped.
|
|
705
|
+
|
|
706
|
+
WHEN NOT TO COMPRESS:
|
|
707
|
+
- Content the current step is actively reading or reasoning about.
|
|
708
|
+
- Important user messages \u2014 preserve their exact intent, constraints, and acceptance criteria.
|
|
709
|
+
- Protected tool outputs \u2014 hard-excluded from compression ranges, survive intact in visible context.
|
|
710
|
+
|
|
711
|
+
{howToCompressRules}
|
|
712
|
+
|
|
713
|
+
Compression tools (refs are SURFACE SEQS, not ids):
|
|
714
|
+
- compress: replace one or more seq ranges, each with your own dense summary. Single range: compress({ content: [{ startSeq, endSeq, summary }] }). Batch multiple unrelated segments in one call (each entry becomes its own block): compress({ content: [{ startSeq: 1, endSeq: 5, summary: '...' }, { startSeq: 12, endSeq: 18, summary: '...' }] }). Keep ranges disjoint \u2014 overlapping entries in one batch are skipped. Edges are auto-balanced to tool-call/result boundaries; a trailing #callId fragment in a seq is ignored. Seq refs must be on the current surface: seqs from older nudges or earlier compresses go stale as the surface moves, so a stale span is auto-remapped to its still-live remainder (the result reports the adjusted span), a fully compressed span is reported as already compressed, and invented/other-session seqs fail with guidance.
|
|
715
|
+
- decompress: recover a compressed block's original content, read-only. decompress({ blockId }).
|
|
716
|
+
- search_context: find information inside compressed blocks BEFORE decompressing. search_context({ query }).
|
|
717
|
+
- acp_status: current context usage and the live compressible-range list. Run it right before compressing \u2014 the only seqs that never go stale are the ones you just read.
|
|
718
|
+
|
|
719
|
+
Tiered compression: each compressed block appears on the surface as one summary node. Compressing that node again DISTILLS the block (tier 2): the parent summary folds into your new summary and the original messages are freed. Distilling a tier-2 block yields tier 3. Distill when a summary itself is consumed \u2014 decompress on the tier-2 block recovers the full originals.
|
|
720
|
+
|
|
721
|
+
{tier2DistillRules}
|
|
722
|
+
|
|
723
|
+
{tier3CondenseRules}
|
|
724
|
+
|
|
725
|
+
When you write a summary, it becomes the ONLY record of that range: keep file paths, signatures, exact values, decisions, and error strings verbatim so a later reader (or you, after decompress) can continue without the original. Never reuse historical seqs \u2014 the surface moves as messages land and compress; verify with acp_status.`
|
|
726
|
+
};
|
|
727
|
+
var DEFAULT_RESOLVED = DEFAULT_PROMPTS;
|
|
728
|
+
|
|
729
|
+
// src/nudge.ts
|
|
588
730
|
function resolveTokenCount(agent, coreMessages) {
|
|
589
731
|
const projections = agent.ctx?.get?.("sessionProjections");
|
|
590
732
|
const projected = projections?.snapshot?.(agent.session)?.values?.contextPressure?.projectedTokens;
|
|
@@ -594,17 +736,24 @@ function resolveTokenCount(agent, coreMessages) {
|
|
|
594
736
|
if (typeof surface === "number" && surface > 0) return surface;
|
|
595
737
|
return coreMessages.reduce((sum, message) => sum + defaultCountTokens2(message.text ?? ""), 0);
|
|
596
738
|
}
|
|
597
|
-
function rangeTable(session) {
|
|
739
|
+
function rangeTable(session, prompts = DEFAULT_RESOLVED) {
|
|
598
740
|
const ranges = buildCompressibleSeqRanges(session).slice(0, 6);
|
|
599
741
|
if (ranges.length === 0) return "";
|
|
600
|
-
const lines = ranges.map(
|
|
742
|
+
const lines = ranges.map(
|
|
743
|
+
(range) => renderTemplate(prompts.rangeTable.line, {
|
|
744
|
+
start: range.start,
|
|
745
|
+
end: range.end,
|
|
746
|
+
count: range.count,
|
|
747
|
+
tokens: range.tokens
|
|
748
|
+
})
|
|
749
|
+
);
|
|
601
750
|
return [
|
|
751
|
+
// 前导空串元素产生 nudge 中范围表前的唯一空行(§4:parts 层不再加分隔)。
|
|
602
752
|
"",
|
|
603
|
-
|
|
604
|
-
|
|
753
|
+
renderTemplate(prompts.rangeTable.header, { surface: surfaceSummary(session) }),
|
|
754
|
+
renderTemplate(prompts.rangeTable.title, { count: ranges.length }),
|
|
605
755
|
...lines,
|
|
606
|
-
|
|
607
|
-
"Snapshot taken at nudge time: the seqs go stale once the surface moves (a later compress shadows them), so re-run acp_status for fresh refs before compressing."
|
|
756
|
+
prompts.rangeTable.footer
|
|
608
757
|
].join("\n");
|
|
609
758
|
}
|
|
610
759
|
function measuredTokenCount(agent, coreMessages) {
|
|
@@ -626,28 +775,112 @@ function buildNudge(agent, env, lastNudgeTurn) {
|
|
|
626
775
|
const alreadyShown = !emergency && lastNudgeTurn.get(session.id) === turnNumber;
|
|
627
776
|
if (alreadyShown) return null;
|
|
628
777
|
lastNudgeTurn.set(session.id, turnNumber);
|
|
629
|
-
const text = buildNudgeText(nudge, emergency, session);
|
|
778
|
+
const text = buildNudgeText(nudge, emergency, session, env.prompts);
|
|
630
779
|
const message = createUserMessage2({
|
|
631
780
|
content: [{ type: "text", text }],
|
|
632
781
|
source: { kind: "plugin", plugin: "acp-nudge" }
|
|
633
782
|
});
|
|
634
783
|
return { message, emergency };
|
|
635
784
|
}
|
|
636
|
-
function buildNudgeText(nudge, emergency, session) {
|
|
785
|
+
function buildNudgeText(nudge, emergency, session, prompts = DEFAULT_RESOLVED) {
|
|
786
|
+
if (prompts.nudge !== DEFAULT_RESOLVED.nudge) {
|
|
787
|
+
return renderNudgeFromTemplates(nudge, emergency, session, prompts);
|
|
788
|
+
}
|
|
789
|
+
const rendered = renderNudgeText(nudge);
|
|
790
|
+
return adaptKernelNudgeToSeq(rendered.text, nudge, session, prompts);
|
|
791
|
+
}
|
|
792
|
+
function adaptKernelNudgeToSeq(text, nudge, session, prompts) {
|
|
793
|
+
let out = text;
|
|
794
|
+
if ((nudge.tier === 2 || nudge.tier === 3) && (nudge.tierTargetBlocks?.length ?? 0) > 0) {
|
|
795
|
+
out = replaceTierTrigger(out, nudge, session, prompts);
|
|
796
|
+
} else if (out.includes('"startId"')) {
|
|
797
|
+
out = replaceEmergencyExample(out);
|
|
798
|
+
}
|
|
799
|
+
const seqTable = rangeTable(session, prompts);
|
|
800
|
+
if (seqTable !== "") out = replaceRangesStr(out, seqTable);
|
|
801
|
+
return out;
|
|
802
|
+
}
|
|
803
|
+
function replaceRangesStr(text, seqTable) {
|
|
804
|
+
const match = text.match(/\n\n(?:Compressible ranges \(|\[No specific ranges detected)/);
|
|
805
|
+
if (!match) return text;
|
|
806
|
+
const start = match.index;
|
|
807
|
+
const rest = text.slice(start + 2);
|
|
808
|
+
const next = rest.match(/\n\n/);
|
|
809
|
+
const end = next !== null ? start + 2 + next.index : text.length;
|
|
810
|
+
const before = text.slice(0, start);
|
|
811
|
+
const after = text.slice(end);
|
|
812
|
+
return before + "\n" + seqTable + after;
|
|
813
|
+
}
|
|
814
|
+
function replaceTierTrigger(text, nudge, session, prompts) {
|
|
815
|
+
const start = text.search(/\n\n(?:\[TIER \d|\[EMERGENCY — TIER \d)/);
|
|
816
|
+
if (start === -1) return text;
|
|
817
|
+
const rest = text.slice(start + 2);
|
|
818
|
+
const next = rest.match(/\n\nHOW TO COMPRESS/);
|
|
819
|
+
const end = next !== null ? start + 2 + next.index : text.length;
|
|
820
|
+
const targets = nudge.tierTargetBlocks;
|
|
821
|
+
const summarySeqs = targets.map((block) => summarySeqOfKernelBlock(session, block.blockId)).filter((seq) => seq !== null);
|
|
822
|
+
const pending = nudge.tier === 2 ? nudge.breakdown?.pendingT2 : nudge.breakdown?.pendingT3;
|
|
823
|
+
const tokens = typeof pending === "number" ? pending : 0;
|
|
824
|
+
const tierValue = nudge.tier === null ? 2 : nudge.tier;
|
|
825
|
+
const tierLine = renderTemplate(prompts.nudge.tier, {
|
|
826
|
+
tier: tierValue,
|
|
827
|
+
count: targets.length,
|
|
828
|
+
prevTier: tierValue - 1,
|
|
829
|
+
tokens,
|
|
830
|
+
seqs: summarySeqs.join(", ")
|
|
831
|
+
});
|
|
832
|
+
return text.slice(0, start) + "\n\n" + tierLine + text.slice(end);
|
|
833
|
+
}
|
|
834
|
+
function replaceEmergencyExample(text) {
|
|
835
|
+
const start = text.search(/\n\n\{ "topic":/);
|
|
836
|
+
if (start === -1) return text;
|
|
837
|
+
const rest = text.slice(start + 2);
|
|
838
|
+
const next = rest.match(/\n\nCompressible ranges |\n\n\[No specific/);
|
|
839
|
+
const end = next !== null ? start + 2 + next.index : text.length;
|
|
840
|
+
return text.slice(0, start) + "\n\ncompress({ content: [{ startSeq, endSeq, summary }] }) \u2014 use the seqs from the range table above." + text.slice(end);
|
|
841
|
+
}
|
|
842
|
+
function renderNudgeFromTemplates(nudge, emergency, session, prompts) {
|
|
637
843
|
const pct = Math.round(Math.min(nudge.contextUsage, 1) * 100);
|
|
638
|
-
const frame =
|
|
639
|
-
|
|
640
|
-
|
|
844
|
+
const frame = renderTemplate(
|
|
845
|
+
emergency ? prompts.nudge.emergency : prompts.nudge.normal,
|
|
846
|
+
{ pct, philosophy: COMPRESS_PHILOSOPHY2 }
|
|
847
|
+
);
|
|
848
|
+
const parts = [frame];
|
|
849
|
+
if (nudge.contextBreakdown) {
|
|
850
|
+
const bd = nudge.contextBreakdown;
|
|
851
|
+
const breakdown = renderTemplate(prompts.nudge.breakdown, {
|
|
852
|
+
system: Math.round(bd.system / 1e3),
|
|
853
|
+
tool: Math.round(bd.tool / 1e3),
|
|
854
|
+
summaries: Math.round(bd.summaries / 1e3),
|
|
855
|
+
code: Math.round(bd.code / 1e3),
|
|
856
|
+
text: Math.round(bd.text / 1e3)
|
|
857
|
+
});
|
|
858
|
+
if (breakdown !== "") parts.push("", breakdown);
|
|
859
|
+
if (bd.growth > 0) {
|
|
860
|
+
const growth = renderTemplate(prompts.nudge.growth, { growth: Math.round(bd.growth / 1e3) });
|
|
861
|
+
if (growth !== "") parts.push(growth);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
if (prompts.nudge.guidance !== "") parts.push("", prompts.nudge.guidance);
|
|
641
865
|
if ((nudge.tier === 2 || nudge.tier === 3) && (nudge.tierTargetBlocks?.length ?? 0) > 0) {
|
|
642
866
|
const targets = nudge.tierTargetBlocks;
|
|
643
867
|
const summarySeqs = targets.map((block) => summarySeqOfKernelBlock(session, block.blockId)).filter((seq) => seq !== null);
|
|
644
868
|
const pending = nudge.tier === 2 ? nudge.breakdown?.pendingT2 : nudge.breakdown?.pendingT3;
|
|
645
869
|
const tokens = typeof pending === "number" ? pending : 0;
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
870
|
+
const tierLine = renderTemplate(prompts.nudge.tier, {
|
|
871
|
+
tier: nudge.tier,
|
|
872
|
+
count: targets.length,
|
|
873
|
+
prevTier: nudge.tier - 1,
|
|
874
|
+
tokens,
|
|
875
|
+
seqs: summarySeqs.join(", ")
|
|
876
|
+
});
|
|
877
|
+
if (tierLine !== "") parts.push(tierLine);
|
|
878
|
+
const tierRules = nudge.tier === 2 ? TIER2_DISTILL_RULES2 : TIER3_CONDENSE_RULES2;
|
|
879
|
+
parts.push("", tierRules);
|
|
880
|
+
} else {
|
|
881
|
+
parts.push(rangeTable(session, prompts));
|
|
649
882
|
}
|
|
650
|
-
parts.push(
|
|
883
|
+
if (prompts.nudge.tip !== "") parts.push("", prompts.nudge.tip);
|
|
651
884
|
return parts.join("\n");
|
|
652
885
|
}
|
|
653
886
|
|
|
@@ -691,11 +924,22 @@ function requireAgent(exec) {
|
|
|
691
924
|
return exec.agent;
|
|
692
925
|
}
|
|
693
926
|
var compressParameters = {
|
|
927
|
+
// Tolerated wrapped-arguments form: some models emit
|
|
928
|
+
// `{ "arguments": "{\"content\": [...]}" }` (double-nested) or
|
|
929
|
+
// `{ "arguments": { "content": [...] } }` instead of the unwrapped
|
|
930
|
+
// `{ "content": [...] }`. The old DSH validator surfaced this as
|
|
931
|
+
// `invalid arguments: "arguments" must be an object` and the model retried
|
|
932
|
+
// forever. `arguments` is accepted as an optional JSON node so the wrapped
|
|
933
|
+
// shape passes schema validation; `handleCompress` unwraps it and falls back
|
|
934
|
+
// to a clear runtime error when neither form carries content. `content` is
|
|
935
|
+
// intentionally NOT `required: true` — a required property would reject the
|
|
936
|
+
// wrapped shape before `handleCompress` can see it. The tool description
|
|
937
|
+
// still tells the model content is mandatory.
|
|
938
|
+
arguments: { type: "json", description: "Tolerated wrapped-arguments form (model-generated); unwrapped in handleCompress. Prefer passing content directly." },
|
|
694
939
|
topic: { type: "string", description: "Fallback topic for entries without their own." },
|
|
695
940
|
content: {
|
|
696
941
|
type: "array",
|
|
697
|
-
|
|
698
|
-
description: "One or more ranges to compress, each with startSeq/endSeq boundaries (surface seqs) and a dense summary.",
|
|
942
|
+
description: "One or more ranges to compress, each with startSeq/endSeq boundaries (surface seqs) and a dense summary. Required \u2014 pass it directly, not wrapped in an arguments key.",
|
|
699
943
|
items: {
|
|
700
944
|
type: "object",
|
|
701
945
|
properties: {
|
|
@@ -726,6 +970,22 @@ function parseSeq(value) {
|
|
|
726
970
|
}
|
|
727
971
|
return seq;
|
|
728
972
|
}
|
|
973
|
+
function unwrapCompressArgs(args) {
|
|
974
|
+
if (args.content !== void 0) return args;
|
|
975
|
+
if (args.arguments === void 0) return null;
|
|
976
|
+
let inner = args.arguments;
|
|
977
|
+
if (typeof inner === "string") {
|
|
978
|
+
try {
|
|
979
|
+
inner = JSON.parse(inner);
|
|
980
|
+
} catch {
|
|
981
|
+
return null;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
if (typeof inner !== "object" || inner === null || Array.isArray(inner)) return null;
|
|
985
|
+
const content = inner.content;
|
|
986
|
+
if (content === void 0) return null;
|
|
987
|
+
return { ...args, content };
|
|
988
|
+
}
|
|
729
989
|
async function handleCompress(env, args, exec) {
|
|
730
990
|
const agent = requireAgent(exec);
|
|
731
991
|
const session = agent.session;
|
|
@@ -737,6 +997,13 @@ async function handleCompress(env, args, exec) {
|
|
|
737
997
|
const turn = env.kernel.processTurn({ messages: coreMessages, state, config, tokenCount });
|
|
738
998
|
env.store.set(session, turn.state);
|
|
739
999
|
const byRaw = turn.state.messageRefs.byRaw;
|
|
1000
|
+
const unwrapped = unwrapCompressArgs(args);
|
|
1001
|
+
if (unwrapped === null) {
|
|
1002
|
+
return {
|
|
1003
|
+
text: "compress: missing content \u2014 pass the content array directly: compress({ content: [{ startSeq, endSeq, summary }] })"
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
args = unwrapped;
|
|
740
1007
|
const ranges = [];
|
|
741
1008
|
const alreadyCompressedNotes = [];
|
|
742
1009
|
for (const range of args.content) {
|
|
@@ -935,10 +1202,11 @@ async function handleStatus(env, _args, exec) {
|
|
|
935
1202
|
return { text: lines.join("\n") };
|
|
936
1203
|
}
|
|
937
1204
|
function makeTools(env) {
|
|
1205
|
+
const prompts = env.prompts ?? DEFAULT_RESOLVED;
|
|
938
1206
|
return [
|
|
939
1207
|
defineTool({
|
|
940
1208
|
name: "compress",
|
|
941
|
-
description:
|
|
1209
|
+
description: prompts.tools.compress,
|
|
942
1210
|
parameters: compressParameters,
|
|
943
1211
|
output: textOutput(),
|
|
944
1212
|
async execute(args, exec) {
|
|
@@ -947,7 +1215,7 @@ function makeTools(env) {
|
|
|
947
1215
|
}),
|
|
948
1216
|
defineTool({
|
|
949
1217
|
name: "decompress",
|
|
950
|
-
description:
|
|
1218
|
+
description: prompts.tools.decompress,
|
|
951
1219
|
parameters: decompressParameters,
|
|
952
1220
|
output: textOutput(),
|
|
953
1221
|
execute(args, exec) {
|
|
@@ -956,7 +1224,7 @@ function makeTools(env) {
|
|
|
956
1224
|
}),
|
|
957
1225
|
defineTool({
|
|
958
1226
|
name: "search_context",
|
|
959
|
-
description:
|
|
1227
|
+
description: prompts.tools.searchContext,
|
|
960
1228
|
parameters: searchParameters,
|
|
961
1229
|
output: textOutput(),
|
|
962
1230
|
execute(args, exec) {
|
|
@@ -965,7 +1233,7 @@ function makeTools(env) {
|
|
|
965
1233
|
}),
|
|
966
1234
|
defineTool({
|
|
967
1235
|
name: "acp_status",
|
|
968
|
-
description:
|
|
1236
|
+
description: prompts.tools.acpStatus,
|
|
969
1237
|
parameters: statusParameters,
|
|
970
1238
|
output: textOutput(),
|
|
971
1239
|
execute(args, exec) {
|
|
@@ -1062,22 +1330,7 @@ function acpCommand(env) {
|
|
|
1062
1330
|
}
|
|
1063
1331
|
|
|
1064
1332
|
// src/system-prompt.ts
|
|
1065
|
-
|
|
1066
|
-
var ACP_SYSTEM_PROMPT = `Active Context Pruning \u2014 model-driven context management
|
|
1067
|
-
|
|
1068
|
-
YOU decide whether and when to compress context. Nothing forces you: the injected "nudge" is a suggestion, not an order, and you may ignore it when compression would not help. Compress only ranges you have genuinely consumed (read tool outputs, finished explorations, superseded steps) that the current work no longer needs verbatim.
|
|
1069
|
-
|
|
1070
|
-
${COMPRESS_PHILOSOPHY}
|
|
1071
|
-
|
|
1072
|
-
Compression tools (refs are SURFACE SEQS, not ids):
|
|
1073
|
-
- compress: replace one or more seq ranges, each with your own dense summary. Single range: compress({ content: [{ startSeq, endSeq, summary }] }). Batch multiple unrelated segments in one call (each entry becomes its own block): compress({ content: [{ startSeq: 1, endSeq: 5, summary: '...' }, { startSeq: 12, endSeq: 18, summary: '...' }] }). Keep ranges disjoint \u2014 overlapping entries in one batch are skipped. Edges are auto-balanced to tool-call/result boundaries; a trailing #callId fragment in a seq is ignored. Seq refs must be on the current surface: seqs from older nudges or earlier compresses go stale as the surface moves, so a stale span is auto-remapped to its still-live remainder (the result reports the adjusted span), a fully compressed span is reported as already compressed, and invented/other-session seqs fail with guidance.
|
|
1074
|
-
- decompress: recover a compressed block's original content, read-only. decompress({ blockId }).
|
|
1075
|
-
- search_context: find information inside compressed blocks BEFORE decompressing. search_context({ query }).
|
|
1076
|
-
- acp_status: current context usage and the live compressible-range list. Run it right before compressing \u2014 the only seqs that never go stale are the ones you just read.
|
|
1077
|
-
|
|
1078
|
-
Tiered compression: each compressed block appears on the surface as one summary node. Compressing that node again DISTILLS the block (tier 2): the parent summary folds into your new summary and the original messages are freed. Distilling a tier-2 block yields tier 3. Distill when a summary itself is consumed \u2014 decompress on the tier-2 block recovers the full originals.
|
|
1079
|
-
|
|
1080
|
-
When you write a summary, it becomes the ONLY record of that range: keep file paths, signatures, exact values, decisions, and error strings verbatim so a later reader (or you, after decompress) can continue without the original. Never reuse historical seqs \u2014 the surface moves as messages land and compress; verify with acp_status.`;
|
|
1333
|
+
var ACP_SYSTEM_PROMPT = renderSystemPrompt(DEFAULT_PROMPTS);
|
|
1081
1334
|
var ACP_SYSTEM_PROMPT_ORDER = 150;
|
|
1082
1335
|
|
|
1083
1336
|
// src/index.ts
|
|
@@ -1104,12 +1357,15 @@ var AcpCompactionEngine = class extends CompactionEngine {
|
|
|
1104
1357
|
store;
|
|
1105
1358
|
/** Resolved engine configuration. */
|
|
1106
1359
|
config;
|
|
1360
|
+
/** Resolved prompt templates (validated at construction — fail-fast on template typos). */
|
|
1361
|
+
prompts;
|
|
1107
1362
|
lastNudgeTurn = /* @__PURE__ */ new Map();
|
|
1108
1363
|
/** Per provider/model route the resolved window (probe failures cached too). */
|
|
1109
1364
|
windowCache = /* @__PURE__ */ new Map();
|
|
1110
1365
|
constructor(ctx, config = {}) {
|
|
1111
1366
|
super(ctx);
|
|
1112
1367
|
this.config = resolveAcpConfig(config);
|
|
1368
|
+
this.prompts = resolvePrompts(config.prompts);
|
|
1113
1369
|
const ports = this.config.countTokens !== void 0 ? { countTokens: this.config.countTokens } : {};
|
|
1114
1370
|
this.kernel = createCore(ports);
|
|
1115
1371
|
this.store = new AcpStateStore();
|
|
@@ -1122,7 +1378,8 @@ var AcpCompactionEngine = class extends CompactionEngine {
|
|
|
1122
1378
|
nudgeMaxContextLimitPct: this.config.nudgeMaxContextLimitPct,
|
|
1123
1379
|
nudgeEmergencyThresholdPct: this.config.nudgeEmergencyThresholdPct,
|
|
1124
1380
|
coreOverrides: this.config.coreOverrides,
|
|
1125
|
-
windowFor: (agent) => this.windowFor(agent)
|
|
1381
|
+
windowFor: (agent) => this.windowFor(agent),
|
|
1382
|
+
prompts: this.prompts
|
|
1126
1383
|
};
|
|
1127
1384
|
const tools = ctx.get("tools");
|
|
1128
1385
|
if (tools !== void 0) {
|
|
@@ -1171,7 +1428,7 @@ var AcpCompactionEngine = class extends CompactionEngine {
|
|
|
1171
1428
|
systemPrompt.section({
|
|
1172
1429
|
name: "billion-context-dsh",
|
|
1173
1430
|
order: ACP_SYSTEM_PROMPT_ORDER,
|
|
1174
|
-
text:
|
|
1431
|
+
text: renderSystemPrompt(this.prompts)
|
|
1175
1432
|
});
|
|
1176
1433
|
} else {
|
|
1177
1434
|
let done = false;
|
|
@@ -1183,7 +1440,7 @@ var AcpCompactionEngine = class extends CompactionEngine {
|
|
|
1183
1440
|
registry.section({
|
|
1184
1441
|
name: "billion-context-dsh",
|
|
1185
1442
|
order: ACP_SYSTEM_PROMPT_ORDER,
|
|
1186
|
-
text:
|
|
1443
|
+
text: renderSystemPrompt(this.prompts)
|
|
1187
1444
|
});
|
|
1188
1445
|
};
|
|
1189
1446
|
ctx.on("internal/service", (name) => {
|
|
@@ -1248,6 +1505,8 @@ export {
|
|
|
1248
1505
|
AcpStateStore,
|
|
1249
1506
|
AlreadyCompressedRangeError,
|
|
1250
1507
|
DEFAULT_CONTEXT_WINDOW,
|
|
1508
|
+
DEFAULT_PROMPTS,
|
|
1509
|
+
DEFAULT_RESOLVED,
|
|
1251
1510
|
acpCommand,
|
|
1252
1511
|
assertNoActiveCompaction,
|
|
1253
1512
|
blockRefForSummarySeq,
|
|
@@ -1264,7 +1523,10 @@ export {
|
|
|
1264
1523
|
makeTools,
|
|
1265
1524
|
projectEvent,
|
|
1266
1525
|
rebuildBlockLedger,
|
|
1526
|
+
renderSystemPrompt,
|
|
1527
|
+
renderTemplate,
|
|
1267
1528
|
resolveAcpConfig,
|
|
1529
|
+
resolvePrompts,
|
|
1268
1530
|
resolveSurfaceRange,
|
|
1269
1531
|
resolveTokenCount,
|
|
1270
1532
|
runCompactionTransaction,
|