billion-context-dsh 0.1.9 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [English](./README.en.md) | [中文](./README.md)
4
4
 
5
5
  > **⚠️ Beta notice — not for production use**
6
- > This project (**v0.1.9**) is a work-in-progress beta. The [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) itself is also in **public beta**. **Do not use either in engineering / production environments** — expect breaking changes and rough edges.
6
+ > This project (**v0.2.0**) is a work-in-progress beta. The [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) itself is also in **public beta**. **Do not use either in engineering / production environments** — expect breaking changes and rough edges.
7
7
 
8
8
  <p align="center">
9
9
  <strong>Built with gratitude on top of these projects</strong> — please give them a ⭐:
@@ -56,6 +56,18 @@ This is the DeepSeek Harness port of [billion-context-pi](https://github.com/ran
56
56
  npm install billion-context-dsh
57
57
  ```
58
58
 
59
+ > 💡 **One-command install via `dsh plugin` (bundle, v0.2.0+)**. The package declares a `dsh.bundle`
60
+ > manifest, so DSH's plugin command installs it into the profile and applies the patch
61
+ > automatically (equivalent to the composition row below):
62
+
63
+ ```bash
64
+ dsh plugin --profile web add billion-context-dsh
65
+ ```
66
+
67
+ Restart `dsh` afterwards (bundle layers are composed at startup). For custom `config`
68
+ (such as `modelContextLimit` / `prompts`), keep the hand-written composition row — the
69
+ bundle patch ([cordis.patch.yml](cordis.patch.yml)) only inserts the default row without `config`.
70
+
59
71
  That's it. Then add a composition row where a compaction backend is expected — two scopes, pick by how wide you want it:
60
72
 
61
73
  **Global — host plane, every mode** (recommended). In your profile patch (e.g. `~/.dsh/profiles/web/cordis.patch.yml`), add:
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [中文](./README.md) | [English](./README.en.md)
4
4
 
5
5
  > **⚠️ 测试版声明——请勿用于生产环境**
6
- > 本项目(**v0.1.9**)仍处于开发中的测试版。[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 本身也处于**公开测试版**阶段。**请勿将两者用于工程化 / 生产环境**——预期会有破坏性变更与粗糙之处。
6
+ > 本项目(**v0.2.0**)仍处于开发中的测试版。[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 本身也处于**公开测试版**阶段。**请勿将两者用于工程化 / 生产环境**——预期会有破坏性变更与粗糙之处。
7
7
 
8
8
  <p align="center">
9
9
  <strong>衷心感谢以下项目——请给它们一个 ⭐:</strong>
@@ -59,6 +59,17 @@
59
59
  npm install billion-context-dsh
60
60
  ```
61
61
 
62
+ > 💡 **v0.2.0 起支持 `dsh plugin` 一键安装(bundle)**。包已声明 `dsh.bundle`
63
+ > manifest,DSH 的插件命令会把它装进 profile 并自动应用补丁(等价于下面的组合行):
64
+
65
+ ```bash
66
+ dsh plugin --profile web add billion-context-dsh
67
+ ```
68
+
69
+ 装完重启 `dsh`(bundle 层在启动时组合)。需要自定义 `config`(如
70
+ `modelContextLimit` / `prompts`)时仍建议手写组合行——bundle 补丁
71
+ ([cordis.patch.yml](cordis.patch.yml))只插入无 `config` 的默认行。
72
+
62
73
  就这样。然后在需要压缩后端的位置加组合配置——两种范围,按需选择:
63
74
 
64
75
  **全局生效(host 平面,所有模式)——推荐**。在你的 profile 补丁(如 `~/.dsh/profiles/web/cordis.patch.yml`)中追加:
@@ -0,0 +1,11 @@
1
+ # dsh bundle patch: inserts the ACP compaction backend into a profile's layer stack.
2
+ #
3
+ # This is what makes the package installable via `dsh plugin --profile web add
4
+ # billion-context-dsh` (declared in package.json as `dsh.bundle.patch`). The row
5
+ # below is exactly the manual composition row documented in docs/INSTALL.md (§2a),
6
+ # minus config: omitting `modelContextLimit` means auto-detection (fallback 128000),
7
+ # and the built-in prompt copy (config.prompts) is used. Users who want custom
8
+ # config can still write the row by hand in their profile patch.
9
+ - insert:
10
+ - id: compaction-acp
11
+ name: 'billion-context-dsh'
package/dist/index.d.ts CHANGED
@@ -30,10 +30,12 @@ import { CompactionEngine, type CompactionAgentContext, type CompactionResult, t
30
30
  import { type CompressionCore } from 'acp-kernel';
31
31
  import type { Agent } from '@deepseek-ai/dsh-agent';
32
32
  import { AcpStateStore } from './state.ts';
33
+ import { type AcpPrompts, type ResolvedPrompts } from './prompts.ts';
33
34
  import { type AcpWindow } from './window.ts';
34
35
  export { AcpStateStore } from './state.ts';
35
36
  export { kernelConfigFor, type KernelConfigInput } from './config.ts';
36
37
  export { ACP_SYSTEM_PROMPT, ACP_SYSTEM_PROMPT_ORDER } from './system-prompt.ts';
38
+ export { DEFAULT_PROMPTS, DEFAULT_RESOLVED, renderSystemPrompt, renderTemplate, resolvePrompts, type AcpPrompts, type NudgePrompts, type PromptInput, type PromptOverride, type RangeTablePrompts, type ResolvedPrompts, type ToolPrompts, } from './prompts.ts';
37
39
  export { makeTools, type ToolEnvironment } from './tools.ts';
38
40
  export { acpCommand } from './commands.ts';
39
41
  export { buildNudge, resolveTokenCount, type NudgeEnvironment, type NudgeOutcome } from './nudge.ts';
@@ -87,6 +89,8 @@ export interface AcpConfig {
87
89
  readonly autoCommand: boolean;
88
90
  /** Inject the nudge into `agent/pre-step` when the kernel recommends it. Default true. */
89
91
  readonly autoNudge: boolean;
92
+ /** Per-stage prompt template overrides (nudge / range table / system prompt / tool descriptions). See docs/configurable-prompts-design.md. */
93
+ readonly prompts?: AcpPrompts;
90
94
  }
91
95
  export declare function resolveAcpConfig(config?: Partial<AcpConfig>): AcpConfig;
92
96
  /**
@@ -101,6 +105,8 @@ export declare class AcpCompactionEngine extends CompactionEngine {
101
105
  readonly store: AcpStateStore;
102
106
  /** Resolved engine configuration. */
103
107
  readonly config: AcpConfig;
108
+ /** Resolved prompt templates (validated at construction — fail-fast on template typos). */
109
+ readonly prompts: ResolvedPrompts;
104
110
  private readonly lastNudgeTurn;
105
111
  /** Per provider/model route the resolved window (probe failures cached too). */
106
112
  private readonly windowCache;
package/dist/index.js CHANGED
@@ -585,6 +585,111 @@ import {
585
585
  defaultCountTokens as defaultCountTokens2
586
586
  } from "acp-kernel";
587
587
  import { createUserMessage as createUserMessage2 } from "@deepseek-ai/dsh-llm";
588
+
589
+ // src/prompts.ts
590
+ import { COMPRESS_PHILOSOPHY } from "acp-kernel";
591
+ var NUDGE_ALLOWED = {
592
+ normal: /* @__PURE__ */ new Set(["pct"]),
593
+ emergency: /* @__PURE__ */ new Set(["pct"]),
594
+ guidance: /* @__PURE__ */ new Set(),
595
+ tier: /* @__PURE__ */ new Set(["tier", "count", "prevTier", "tokens", "seqs"])
596
+ };
597
+ var RANGE_TABLE_ALLOWED = {
598
+ header: /* @__PURE__ */ new Set(["surface"]),
599
+ title: /* @__PURE__ */ new Set(["count"]),
600
+ line: /* @__PURE__ */ new Set(["start", "end", "count", "tokens"]),
601
+ footer: /* @__PURE__ */ new Set()
602
+ };
603
+ var TOOLS_ALLOWED = {
604
+ compress: /* @__PURE__ */ new Set(),
605
+ decompress: /* @__PURE__ */ new Set(),
606
+ searchContext: /* @__PURE__ */ new Set(),
607
+ acpStatus: /* @__PURE__ */ new Set()
608
+ };
609
+ var SYSTEM_ALLOWED = /* @__PURE__ */ new Set(["philosophy"]);
610
+ function validateTemplate(template, allowed, path) {
611
+ const re = /\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
612
+ let match;
613
+ while ((match = re.exec(template)) !== null) {
614
+ const name = match[1];
615
+ if (!allowed.has(name)) {
616
+ throw new Error(
617
+ `${path} contains unknown placeholder {${name}} \u2014 allowed: ${[...allowed].join(", ") || "(none)"}`
618
+ );
619
+ }
620
+ }
621
+ return template;
622
+ }
623
+ function renderTemplate(template, vars) {
624
+ return template.replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, name) => {
625
+ const value = vars[name];
626
+ if (value === void 0) {
627
+ throw new Error(
628
+ `renderTemplate: missing value for placeholder {${name}} in template "${template.slice(0, 60)}\u2026"`
629
+ );
630
+ }
631
+ return String(value);
632
+ });
633
+ }
634
+ function mergeGroup(defaults, override, allowed, path) {
635
+ if (override == null) return defaults;
636
+ const out = {};
637
+ for (const key of Object.keys(defaults)) {
638
+ const value = override[key];
639
+ out[key] = value === null || value === void 0 ? defaults[key] : validateTemplate(value, allowed[key], `${path}.${String(key)}`);
640
+ }
641
+ return out;
642
+ }
643
+ function resolvePrompts(input) {
644
+ if (input === void 0) return DEFAULT_RESOLVED;
645
+ return {
646
+ nudge: mergeGroup(DEFAULT_PROMPTS.nudge, input.nudge, NUDGE_ALLOWED, "prompts.nudge"),
647
+ rangeTable: mergeGroup(DEFAULT_PROMPTS.rangeTable, input.rangeTable, RANGE_TABLE_ALLOWED, "prompts.rangeTable"),
648
+ tools: mergeGroup(DEFAULT_PROMPTS.tools, input.tools, TOOLS_ALLOWED, "prompts.tools"),
649
+ systemPromptTemplate: input.systemPrompt === null || input.systemPrompt === void 0 ? DEFAULT_PROMPTS.systemPromptTemplate : validateTemplate(input.systemPrompt, SYSTEM_ALLOWED, "prompts.systemPrompt")
650
+ };
651
+ }
652
+ function renderSystemPrompt(prompts) {
653
+ return renderTemplate(prompts.systemPromptTemplate, { philosophy: COMPRESS_PHILOSOPHY });
654
+ }
655
+ var DEFAULT_PROMPTS = {
656
+ nudge: {
657
+ normal: "Context usage is at {pct}%. This is a suggestion, not a requirement \u2014 you decide whether and when to compress.",
658
+ emergency: "\u26A0\uFE0F Context usage is at {pct}% of the window \u2014 nearly full. Consider compressing consumed ranges soon so working context stays available; the choice and timing are yours.",
659
+ guidance: "Compress by need, not by percentage: replace only ranges you have genuinely consumed, with dense self-contained summaries.",
660
+ tier: "Tier {tier}: {count} tier-{prevTier} block(s) distillable ({tokens} tokens) \u2014 compress their summary node(s) [seqs {seqs}] to reclaim the original messages."
661
+ },
662
+ rangeTable: {
663
+ header: "Surface: {surface}",
664
+ title: "Compressible ranges (suggestions only \u2014 compress any consumed span; refs are surface seqs):",
665
+ line: " - seq {start}..{end} \u2014 {count} messages, ~{tokens} tokens",
666
+ 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."
667
+ },
668
+ tools: {
669
+ 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.",
670
+ decompress: "Recover the original content of a compressed block by its blockId (read-only; does not unshadow the range).",
671
+ searchContext: "Search inside compressed blocks (summaries and original content) for information the model no longer sees in context.",
672
+ acpStatus: "Report the ACP block ledger: compressed blocks, reclaimed tokens, and current context pressure."
673
+ },
674
+ systemPromptTemplate: `Active Context Pruning \u2014 model-driven context management
675
+
676
+ 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.
677
+
678
+ {philosophy}
679
+
680
+ Compression tools (refs are SURFACE SEQS, not ids):
681
+ - 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.
682
+ - decompress: recover a compressed block's original content, read-only. decompress({ blockId }).
683
+ - search_context: find information inside compressed blocks BEFORE decompressing. search_context({ query }).
684
+ - 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.
685
+
686
+ 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.
687
+
688
+ 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.`
689
+ };
690
+ var DEFAULT_RESOLVED = DEFAULT_PROMPTS;
691
+
692
+ // src/nudge.ts
588
693
  function resolveTokenCount(agent, coreMessages) {
589
694
  const projections = agent.ctx?.get?.("sessionProjections");
590
695
  const projected = projections?.snapshot?.(agent.session)?.values?.contextPressure?.projectedTokens;
@@ -594,17 +699,24 @@ function resolveTokenCount(agent, coreMessages) {
594
699
  if (typeof surface === "number" && surface > 0) return surface;
595
700
  return coreMessages.reduce((sum, message) => sum + defaultCountTokens2(message.text ?? ""), 0);
596
701
  }
597
- function rangeTable(session) {
702
+ function rangeTable(session, prompts = DEFAULT_RESOLVED) {
598
703
  const ranges = buildCompressibleSeqRanges(session).slice(0, 6);
599
704
  if (ranges.length === 0) return "";
600
- const lines = ranges.map((range) => ` - seq ${range.start}..${range.end} \u2014 ${range.count} messages, ~${range.tokens} tokens`);
705
+ const lines = ranges.map(
706
+ (range) => renderTemplate(prompts.rangeTable.line, {
707
+ start: range.start,
708
+ end: range.end,
709
+ count: range.count,
710
+ tokens: range.tokens
711
+ })
712
+ );
601
713
  return [
714
+ // 前导空串元素产生 nudge 中范围表前的唯一空行(§4:parts 层不再加分隔)。
602
715
  "",
603
- `Surface: ${surfaceSummary(session)}`,
604
- "Compressible ranges (suggestions only \u2014 compress any consumed span; refs are surface seqs):",
716
+ renderTemplate(prompts.rangeTable.header, { surface: surfaceSummary(session) }),
717
+ renderTemplate(prompts.rangeTable.title, { count: ranges.length }),
605
718
  ...lines,
606
- "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.",
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."
719
+ prompts.rangeTable.footer
608
720
  ].join("\n");
609
721
  }
610
722
  function measuredTokenCount(agent, coreMessages) {
@@ -626,28 +738,36 @@ function buildNudge(agent, env, lastNudgeTurn) {
626
738
  const alreadyShown = !emergency && lastNudgeTurn.get(session.id) === turnNumber;
627
739
  if (alreadyShown) return null;
628
740
  lastNudgeTurn.set(session.id, turnNumber);
629
- const text = buildNudgeText(nudge, emergency, session);
741
+ const text = buildNudgeText(nudge, emergency, session, env.prompts);
630
742
  const message = createUserMessage2({
631
743
  content: [{ type: "text", text }],
632
744
  source: { kind: "plugin", plugin: "acp-nudge" }
633
745
  });
634
746
  return { message, emergency };
635
747
  }
636
- function buildNudgeText(nudge, emergency, session) {
748
+ function buildNudgeText(nudge, emergency, session, prompts = DEFAULT_RESOLVED) {
637
749
  const pct = Math.round(Math.min(nudge.contextUsage, 1) * 100);
638
- const frame = emergency ? `\u26A0\uFE0F Context usage is at ${pct}% of the window \u2014 nearly full. Consider compressing consumed ranges soon so working context stays available; the choice and timing are yours.` : `Context usage is at ${pct}%. This is a suggestion, not a requirement \u2014 you decide whether and when to compress.`;
639
- const guidance = "Compress by need, not by percentage: replace only ranges you have genuinely consumed, with dense self-contained summaries.";
640
- const parts = [frame, "", guidance];
750
+ const frame = renderTemplate(
751
+ emergency ? prompts.nudge.emergency : prompts.nudge.normal,
752
+ { pct }
753
+ );
754
+ const parts = [frame];
755
+ if (prompts.nudge.guidance !== "") parts.push("", prompts.nudge.guidance);
641
756
  if ((nudge.tier === 2 || nudge.tier === 3) && (nudge.tierTargetBlocks?.length ?? 0) > 0) {
642
757
  const targets = nudge.tierTargetBlocks;
643
758
  const summarySeqs = targets.map((block) => summarySeqOfKernelBlock(session, block.blockId)).filter((seq) => seq !== null);
644
759
  const pending = nudge.tier === 2 ? nudge.breakdown?.pendingT2 : nudge.breakdown?.pendingT3;
645
760
  const tokens = typeof pending === "number" ? pending : 0;
646
- parts.push(
647
- `Tier ${nudge.tier}: ${targets.length} tier-${nudge.tier - 1} block(s) distillable (${tokens} tokens) \u2014 compress their summary node(s) [seqs ${summarySeqs.join(", ")}] to reclaim the original messages.`
648
- );
761
+ const tierLine = renderTemplate(prompts.nudge.tier, {
762
+ tier: nudge.tier,
763
+ count: targets.length,
764
+ prevTier: nudge.tier - 1,
765
+ tokens,
766
+ seqs: summarySeqs.join(", ")
767
+ });
768
+ if (tierLine !== "") parts.push(tierLine);
649
769
  }
650
- parts.push(rangeTable(session));
770
+ parts.push(rangeTable(session, prompts));
651
771
  return parts.join("\n");
652
772
  }
653
773
 
@@ -935,10 +1055,11 @@ async function handleStatus(env, _args, exec) {
935
1055
  return { text: lines.join("\n") };
936
1056
  }
937
1057
  function makeTools(env) {
1058
+ const prompts = env.prompts ?? DEFAULT_RESOLVED;
938
1059
  return [
939
1060
  defineTool({
940
1061
  name: "compress",
941
- description: "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.",
1062
+ description: prompts.tools.compress,
942
1063
  parameters: compressParameters,
943
1064
  output: textOutput(),
944
1065
  async execute(args, exec) {
@@ -947,7 +1068,7 @@ function makeTools(env) {
947
1068
  }),
948
1069
  defineTool({
949
1070
  name: "decompress",
950
- description: "Recover the original content of a compressed block by its blockId (read-only; does not unshadow the range).",
1071
+ description: prompts.tools.decompress,
951
1072
  parameters: decompressParameters,
952
1073
  output: textOutput(),
953
1074
  execute(args, exec) {
@@ -956,7 +1077,7 @@ function makeTools(env) {
956
1077
  }),
957
1078
  defineTool({
958
1079
  name: "search_context",
959
- description: "Search inside compressed blocks (summaries and original content) for information the model no longer sees in context.",
1080
+ description: prompts.tools.searchContext,
960
1081
  parameters: searchParameters,
961
1082
  output: textOutput(),
962
1083
  execute(args, exec) {
@@ -965,7 +1086,7 @@ function makeTools(env) {
965
1086
  }),
966
1087
  defineTool({
967
1088
  name: "acp_status",
968
- description: "Report the ACP block ledger: compressed blocks, reclaimed tokens, and current context pressure.",
1089
+ description: prompts.tools.acpStatus,
969
1090
  parameters: statusParameters,
970
1091
  output: textOutput(),
971
1092
  execute(args, exec) {
@@ -1062,22 +1183,7 @@ function acpCommand(env) {
1062
1183
  }
1063
1184
 
1064
1185
  // src/system-prompt.ts
1065
- import { COMPRESS_PHILOSOPHY } from "acp-kernel";
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.`;
1186
+ var ACP_SYSTEM_PROMPT = renderSystemPrompt(DEFAULT_PROMPTS);
1081
1187
  var ACP_SYSTEM_PROMPT_ORDER = 150;
1082
1188
 
1083
1189
  // src/index.ts
@@ -1104,12 +1210,15 @@ var AcpCompactionEngine = class extends CompactionEngine {
1104
1210
  store;
1105
1211
  /** Resolved engine configuration. */
1106
1212
  config;
1213
+ /** Resolved prompt templates (validated at construction — fail-fast on template typos). */
1214
+ prompts;
1107
1215
  lastNudgeTurn = /* @__PURE__ */ new Map();
1108
1216
  /** Per provider/model route the resolved window (probe failures cached too). */
1109
1217
  windowCache = /* @__PURE__ */ new Map();
1110
1218
  constructor(ctx, config = {}) {
1111
1219
  super(ctx);
1112
1220
  this.config = resolveAcpConfig(config);
1221
+ this.prompts = resolvePrompts(config.prompts);
1113
1222
  const ports = this.config.countTokens !== void 0 ? { countTokens: this.config.countTokens } : {};
1114
1223
  this.kernel = createCore(ports);
1115
1224
  this.store = new AcpStateStore();
@@ -1122,7 +1231,8 @@ var AcpCompactionEngine = class extends CompactionEngine {
1122
1231
  nudgeMaxContextLimitPct: this.config.nudgeMaxContextLimitPct,
1123
1232
  nudgeEmergencyThresholdPct: this.config.nudgeEmergencyThresholdPct,
1124
1233
  coreOverrides: this.config.coreOverrides,
1125
- windowFor: (agent) => this.windowFor(agent)
1234
+ windowFor: (agent) => this.windowFor(agent),
1235
+ prompts: this.prompts
1126
1236
  };
1127
1237
  const tools = ctx.get("tools");
1128
1238
  if (tools !== void 0) {
@@ -1171,7 +1281,7 @@ var AcpCompactionEngine = class extends CompactionEngine {
1171
1281
  systemPrompt.section({
1172
1282
  name: "billion-context-dsh",
1173
1283
  order: ACP_SYSTEM_PROMPT_ORDER,
1174
- text: ACP_SYSTEM_PROMPT
1284
+ text: renderSystemPrompt(this.prompts)
1175
1285
  });
1176
1286
  } else {
1177
1287
  let done = false;
@@ -1183,7 +1293,7 @@ var AcpCompactionEngine = class extends CompactionEngine {
1183
1293
  registry.section({
1184
1294
  name: "billion-context-dsh",
1185
1295
  order: ACP_SYSTEM_PROMPT_ORDER,
1186
- text: ACP_SYSTEM_PROMPT
1296
+ text: renderSystemPrompt(this.prompts)
1187
1297
  });
1188
1298
  };
1189
1299
  ctx.on("internal/service", (name) => {
@@ -1248,6 +1358,8 @@ export {
1248
1358
  AcpStateStore,
1249
1359
  AlreadyCompressedRangeError,
1250
1360
  DEFAULT_CONTEXT_WINDOW,
1361
+ DEFAULT_PROMPTS,
1362
+ DEFAULT_RESOLVED,
1251
1363
  acpCommand,
1252
1364
  assertNoActiveCompaction,
1253
1365
  blockRefForSummarySeq,
@@ -1264,7 +1376,10 @@ export {
1264
1376
  makeTools,
1265
1377
  projectEvent,
1266
1378
  rebuildBlockLedger,
1379
+ renderSystemPrompt,
1380
+ renderTemplate,
1267
1381
  resolveAcpConfig,
1382
+ resolvePrompts,
1268
1383
  resolveSurfaceRange,
1269
1384
  resolveTokenCount,
1270
1385
  runCompactionTransaction,